Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
chore: proxy login through the server (#10)
Browse files Browse the repository at this point in the history
* fix: make path type not nullable

* chore: proxy login through the server

Co-authored-by: Samer Buna <samerbuna@users.noreply.github.com>
  • Loading branch information
samerbuna and samerbuna committed Dec 20, 2021
1 parent 16cd260 commit ec8b5ad
Show file tree
Hide file tree
Showing 18 changed files with 175 additions and 131 deletions.
3 changes: 2 additions & 1 deletion .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"PORT": "1234",
"GRAPHQL_URI": "http://localhost:4002/graphql",
"GRAPHQL_SUBSCRIPTION_URI": "ws://localhost:4002/graphql",
"SESSION_KEYS": "sessionkeys"
"SESSION_KEYS": "sessionkeys",
"AUTH_ENDPOINT": "http://localhost:1234/api/login"
}
5 changes: 2 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ module.exports = {
"max-nested-callbacks": ["error", { max: 2 }],
"max-params": ["error", { max: 2 }],
"max-statements-per-line": ["error", { max: 1 }],
"max-statements": ["error", { max: 10 }],
"new-cap": "error",
"max-statements": ["error", { max: 21 }],
"new-parens": "error",
"newline-per-chained-call": "error",
"no-alert": "error",
Expand All @@ -88,7 +87,7 @@ module.exports = {
"no-bitwise": "error",
"no-caller": "error",
"no-confusing-arrow": "error",
"no-console": ["error", { allow: ["error", "info"] }],
"no-console": ["warn", { allow: ["error", "info"] }],
"no-constructor-return": "error",
"no-continue": "error",
"no-div-regex": "error",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"tsc:check": "tsc --pretty --noEmit"
},
"dependencies": {
"@urql/core": "^2.3.6",
"autoprefixer": "^10.4.0",
"body-parser": "^1.19.0",
"cookie-session": "^2.0.0",
Expand Down
5 changes: 2 additions & 3 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useContext } from "react"
import GwwContext from "store"
import { useAppState } from "store"
import Balance from "./balance"
import Link from "./link"
import Logout from "./logout"

const Header = ({ balance }: { balance: number }) => {
const { state } = useContext<GwwContextType>(GwwContext)
const { state } = useAppState()

return (
<div className="header">
Expand Down
7 changes: 4 additions & 3 deletions src/components/home.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext } from "react"
import GwwContext from "store"
import { gql, useQuery } from "urql"

import { useAppState } from "store"
import Header from "./header"

const QUERY_ME = gql`
Expand All @@ -18,8 +18,9 @@ const QUERY_ME = gql`
}
}
`

const Home = () => {
const { state } = useContext<GwwContextType>(GwwContext)
const { state } = useAppState()

const [result] = useQuery({
query: QUERY_ME,
Expand Down
2 changes: 1 addition & 1 deletion src/components/link.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import history from "store/history"
import { history } from "store"

type Props = {
to: RoutePath
Expand Down
48 changes: 10 additions & 38 deletions src/components/login.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import fetch from "cross-fetch"
import intlTelInput from "intl-tel-input"
import { gql, useMutation } from "urql"
import React, { useCallback, useRef, useState } from "react"
import history from "store/history"

const MUTATION_USER_LOGIN = gql`
mutation userLogin($input: UserLoginInput!) {
userLogin(input: $input) {
errors {
message
}
authToken
}
}
`
import config from "server/config"
import { history, useAppState } from "store"

const PhoneNumber = ({ onSuccess }: { onSuccess: (arg: string) => void }) => {
const iti = useRef<intlTelInput.Plugin | null>(null)

Expand Down Expand Up @@ -65,7 +54,7 @@ const PhoneNumber = ({ onSuccess }: { onSuccess: (arg: string) => void }) => {
}

const AuthCode = ({ phoneNumber }: { phoneNumber: string }) => {
const [{ fetching }, sendLoginMutation] = useMutation(MUTATION_USER_LOGIN)
const { request } = useAppState()
const [errorMessage, setErrorMessage] = useState("")

const handleAuthCodeSubmit: React.FormEventHandler<HTMLFormElement> = async (event) => {
Expand All @@ -74,33 +63,17 @@ const AuthCode = ({ phoneNumber }: { phoneNumber: string }) => {

const authCode = event.currentTarget.authCode.value

const { error, data } = await sendLoginMutation({
input: {
phone: phoneNumber,
code: authCode,
},
const data = await request.post(config.authEndpoint, {
phoneNumber,
authCode,
})

if (error || data?.userLogin?.errors?.length > 0 || !data?.userLogin?.authToken) {
setErrorMessage(
error?.message ||
data?.userLogin?.errors?.[0].message ||
"Something went wrong. Please try again later.",
)
if (data instanceof Error) {
setErrorMessage(data.message)
return
}

const authToken = data?.userLogin?.authToken

fetch("/api/login", {
method: "post",
headers: {
"Content-Type": "application/json",
"authorization": `Bearer ${authToken}`,
},
})

history.push("/", { authToken })
history.push("/", { authToken: data?.authToken })
}

return (
Expand All @@ -118,7 +91,6 @@ const AuthCode = ({ phoneNumber }: { phoneNumber: string }) => {
onChange={() => setErrorMessage("")}
/>
</form>
{fetching && <div className="loading">...</div>}
{errorMessage && <div className="error">{errorMessage}</div>}
</div>
)
Expand Down
15 changes: 4 additions & 11 deletions src/components/logout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import fetch from "cross-fetch"
import { useContext } from "react"
import GwwContext from "store"
import { useAppState } from "store"

const Logout = () => {
const { dispatch } = useContext<GwwContextType>(GwwContext)
const { dispatch, request } = useAppState()

const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (event) => {
event.preventDefault()
fetch("/api/logout", {
method: "post",
headers: {
"Content-Type": "application/json",
},
})
await request.post("/api/logout")
dispatch({ type: "logout" })
}

Expand Down
11 changes: 5 additions & 6 deletions src/components/root.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { createClient, Provider } from "urql"
import { useContext, useEffect, useReducer } from "react"
import { useEffect, useReducer } from "react"

import GwwContext from "../store"
import history from "store/history"
import { GwwContext, history, useAppState } from "store"
import appRoutes, { SupportedRoutes } from "server/routes"
import config from "server/config"

const Root = () => {
const { state } = useContext<GwwContextType>(GwwContext)
const { state } = useAppState()
if (!state.path) {
return null
}
Expand All @@ -17,7 +16,7 @@ const Root = () => {
)

if (!checkedRoutePath) {
throw new Error("Invaild Route")
throw new Error("INVALID_ROUTE")
}

const Component = appRoutes[checkedRoutePath].component
Expand Down Expand Up @@ -47,7 +46,7 @@ const wwReducer = (state: GwwState, action: GwwAction): GwwState => {
case "logout":
return { ...state, authToken: undefined }
default:
throw new Error()
throw new Error("UNSUPPORTED_ACTION")
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "../styles/index.css"
const container = document.getElementById("root")

if (!container) {
throw new Error("HTML root element is missing")
throw new Error("HTML_ROOT_ELEMENT_IS_MISSING")
}

ReactDOM.hydrateRoot(container, <Root initialState={window.__G_DATA.initialState} />)
69 changes: 69 additions & 0 deletions src/server/api-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import "cross-fetch/polyfill" // The URQL client depends on fetch
import express from "express"
import { gql, createClient } from "@urql/core"

import config from "./config"

const apiRouter = express.Router({ caseSensitive: true })

const client = (req: Express.Request) =>
createClient({
url: config.graphqlUri,
fetchOptions: () => {
const token = req.session?.authToken
return {
headers: { authorization: token ? `Bearer ${token}` : "" },
}
},
})

const MUTATION_USER_LOGIN = gql`
mutation userLogin($input: UserLoginInput!) {
userLogin(input: $input) {
errors {
message
}
authToken
}
}
`

apiRouter.post("/login", async (req, res) => {
try {
const { phoneNumber, authCode } = req.body

if (!phoneNumber || !authCode) {
throw new Error("INVALID_LOGIN_REQUEST")
}

const { error, data } = await client(req)
.mutation(MUTATION_USER_LOGIN, {
input: { phone: phoneNumber, code: authCode },
})
.toPromise()

if (error || data?.userLogin?.errors?.length > 0 || !data?.userLogin?.authToken) {
throw new Error(data?.userLogin?.errors?.[0].message || "SOMETHING_WENT_WRONG")
}

const authToken = data?.userLogin?.authToken

req.session = req.session || {}
req.session.authToken = authToken

return res.send({ authToken })
} catch (err) {
console.error(err)
return res
.status(500)
.send({ error: err instanceof Error ? err.message : "SOMETHING_WENT_WRONG" })
}
})

apiRouter.post("/logout", async (req, res) => {
req.session = req.session || {}
req.session.authToken = null
return res.send({ authToken: null })
})

export default apiRouter
3 changes: 3 additions & 0 deletions src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const requiredEnvVars = [
"PORT",
"GRAPHQL_URI",
"GRAPHQL_SUBSCRIPTION_URI",
"AUTH_ENDPOINT",
]

requiredEnvVars.forEach((envVar) => {
Expand All @@ -25,4 +26,6 @@ export default {

graphqlUri: process.env.GRAPHQL_URI as string,
graphqlSubscriptionUri: process.env.GRAPHQL_SUBSCRIPTION_URI as string,

authEndpoint: process.env.AUTH_ENDPOINT as string,
}
43 changes: 4 additions & 39 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import morgan from "morgan"
import serialize from "serialize-javascript"

import config from "./config"
import { serverRenderer } from "renderers/server"
import { SupportedRoutes } from "./routes"
import apiRouter from "./api-router"
import ssrRouter from "./ssr-router"

const app = express()
app.enable("trust proxy")
Expand Down Expand Up @@ -55,43 +55,8 @@ if (config.isDev) {
}
}

app.post("/api/login", async (req, res) => {
try {
const authToken = req.headers.authorization?.slice(7)
req.session = req.session || {}
req.session.authToken = authToken
return res.send({ authToken })
} catch (err) {
console.error(err)
return res.status(500).send("Server error")
}
})

app.post("/api/logout", async (req, res) => {
req.session = req.session || {}
req.session.authToken = null
return res.send({ authToken: null })
})

app.get("/*", async (req, res) => {
try {
const routePath = req.path
const checkedRoutePath = SupportedRoutes.find(
(supportedRoute) => supportedRoute === routePath,
)
if (!checkedRoutePath) {
return res.status(404)
}
const vars = await serverRenderer({
path: checkedRoutePath,
authToken: req.session?.authToken,
})
return res.render("index", vars)
} catch (err) {
console.error(err)
return res.status(500).send("Server error")
}
})
app.use("/api", apiRouter)
app.use("/", ssrRouter)

app.listen(config.port, config.host, () => {
console.info(`Running on ${config.host}:${config.port}...`)
Expand Down
28 changes: 28 additions & 0 deletions src/server/ssr-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import express from "express"

import { serverRenderer } from "renderers/server"
import { SupportedRoutes } from "./routes"

const ssrRouter = express.Router({ caseSensitive: true })

ssrRouter.get("/*", async (req, res) => {
try {
const routePath = req.path
const checkedRoutePath = SupportedRoutes.find(
(supportedRoute) => supportedRoute === routePath,
)
if (!checkedRoutePath) {
return res.status(404)
}
const vars = await serverRenderer({
path: checkedRoutePath,
authToken: req.session?.authToken,
})
return res.render("index", vars)
} catch (err) {
console.error(err)
return res.status(500).send("Server error")
}
})

export default ssrRouter
Loading

0 comments on commit ec8b5ad

Please sign in to comment.