Skip to content

Commit

Permalink
Refactored authentication & workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
phisn committed Nov 5, 2024
1 parent 7bc8677 commit 329ce24
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 99 deletions.
22 changes: 10 additions & 12 deletions packages/server/src/features/user/user-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { decode, verify } from "@tsndr/cloudflare-worker-jwt"
import { MiddlewareHandler } from "hono"
import { HTTPException } from "hono/http-exception"
import { Environment } from "../../env"
import { JwtToken } from "./user-model"
import { jwtToken } from "./user-model"

export const middlewareAuth: MiddlewareHandler<Environment> = async (c, next) => {
const authorization = c.req.header("Authorization")
Expand All @@ -15,26 +16,23 @@ export const middlewareAuth: MiddlewareHandler<Environment> = async (c, next) =>
const verified = await verify(authorization, c.env.ENV_JWT_SECRET)

if (verified === false) {
return c.body(null, 401)
throw new HTTPException(401)
}

const token = decode<JwtToken>(authorization)
const token = jwtToken.safeParse(decode(authorization))

if (token.payload === undefined) {
return c.body(null, 401)
if (token.success === false) {
throw new HTTPException(401)
}

const WEEK_IN_MS = 1000 * 60 * 60 * 24 * 7

if (token.payload.iat < Date.now() - WEEK_IN_MS) {
return c.body(null, 401)
if (token.data.iat < Date.now() - WEEK_IN_MS) {
throw new HTTPException(401)
}

if (token.payload.type === "login") {
c.set("userId", token.payload.userId)
}

c.set("jwtToken", token.payload)
c.set("userId", token.data.userId)
c.set("jwtToken", token.data)

return next()
}
25 changes: 13 additions & 12 deletions packages/server/src/features/user/user-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ export const userDTO = (user: User): UserDTO => ({
username: user.username,
})

export const jwtToken = z.union([
z.object({
iat: z.number(),
type: z.literal("login"),
userId: z.number(),
}),
z.object({
iat: z.number(),
type: z.literal("signup"),
email: z.string(),
}),
])
export const jwtToken = z.object({
type: z.literal("signin"),
iat: z.number(),
userId: z.number(),
})

export type JwtToken = z.infer<typeof jwtToken>

export const signupToken = z.object({
type: z.literal("signup"),
iat: z.number(),
email: z.string(),
})

export type SignupToken = z.infer<typeof signupToken>
48 changes: 31 additions & 17 deletions packages/server/src/features/user/user.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { zValidator } from "@hono/zod-validator"
import jwt from "@tsndr/cloudflare-worker-jwt"
import jwt, { decode, verify } from "@tsndr/cloudflare-worker-jwt"
import { eq } from "drizzle-orm"
import { Hono } from "hono"
import { HTTPException } from "hono/http-exception"
import { OAuth2Client } from "oslo/oauth2"
import { UserDTO } from "shared/src/server/user"
import { z } from "zod"
import { Environment } from "../../env"
import { JwtToken, userDTO, users } from "./user-model"
import { JwtToken, SignupToken, signupToken, userDTO, users } from "./user-model"
import { UserService } from "./user-service"

export const routeUser = new Hono<Environment>()
Expand Down Expand Up @@ -80,10 +80,10 @@ export const routeUser = new Hono<Environment>()

if (user === undefined) {
return c.json({
token: await jwt.sign<JwtToken>(
token: await jwt.sign<SignupToken>(
{
iat: Date.now(),
type: "signup",
iat: Date.now(),
email: userInfo.data.email,
},
c.env.ENV_JWT_SECRET,
Expand All @@ -94,8 +94,8 @@ export const routeUser = new Hono<Environment>()
return c.json({
token: await jwt.sign<JwtToken>(
{
type: "signin",
iat: Date.now(),
type: "login",
userId: user.id,
},
c.env.ENV_JWT_SECRET,
Expand All @@ -110,21 +110,28 @@ export const routeUser = new Hono<Environment>()
zValidator(
"json",
z.object({
code: z.string(),
username: z.string(),
}),
),
async c => {
const jwtToken = c.get("jwtToken")
const db = c.get("db")
const json = c.req.valid("json")

if (jwtToken?.type !== "signup") {
return c.body(null, 401)
const verified = await verify(json.code, c.env.ENV_JWT_SECRET)

if (verified === false) {
throw new HTTPException(401)
}

const db = c.get("db")
const json = c.req.valid("json")
const token = signupToken.safeParse(decode(json.code))

if (token.success === false) {
throw new HTTPException(401)
}

const user = {
email: jwtToken.email,
email: token.data.email,
username: json.username,
} as const

Expand All @@ -145,11 +152,18 @@ export const routeUser = new Hono<Environment>()
)
}

return c.json({
currentUser: userDTO({
...response,
...user,
}),
})
return c.json(
{
token: await jwt.sign<JwtToken>(
{
type: "signin",
iat: Date.now(),
userId: response.id,
},
c.env.ENV_JWT_SECRET,
),
},
200,
)
},
)
69 changes: 31 additions & 38 deletions packages/web/src/common/hooks/UseAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,60 @@ import { useGoogleLogin } from "@react-oauth/google"
import { useMemo, useState } from "react"
import { ModalCreateAccount } from "../../components/modals/ModalCreateAccount"
import { ModalRenameAccount } from "../../components/modals/ModalRenameAccount"
import { useAppStore } from "../store/app-store"
import { trpcNative } from "../trpc/trpc-native"

export enum AuthState {
Unauthenticated = "unauthenticated",
Authenticated = "authenticated",
Pending = "pending",
}
import { authService } from "../services/auth-service"
import { rpc } from "../services/rpc"
import { useStore } from "../store"

export interface AuthApi {
login: () => void
rename: () => void
}

export function useAuth(): [AuthState, AuthApi] {
const [me, jwt, hasHydrated] = useAppStore(state => [
state.currentUser,
state.currentUserJwt,
state.hydrated,
])
export function useAuth(): AuthApi {
const [loading, setLoading] = useState(false)

const googleLogin = useGoogleLogin({
onSuccess: async ({ code }) => {
try {
const response = await trpcNative.user.getToken.query({ code })
const response = await rpc.user.signin.$post({
json: {
code,
},
})

if (!response.ok) {
console.error("Failed to signin", response)

useStore.getState().newAlert({
type: "error",
message: `Failed to signin (${response.status})`,
})

setLoading(false)
}

if (response.type === "prompt-create") {
useAppStore.getState().newModal({
const responseJson = await response.json()

if (responseJson.type === "prompt-create") {
useStore.getState().newModal({
modal: function CreateModal() {
return (
<ModalCreateAccount
creationJwt={response.tokenForCreation}
creationJwt={responseJson.token}
onCancel={() => {
useAppStore.getState().removeModal()
useStore.getState().removeModal()
setLoading(false)
}}
onCreated={() => {
useAppStore.getState().removeModal()
useStore.getState().removeModal()
setLoading(false)
}}
/>
)
},
})
} else {
useAppStore.getState().updateJwt(response.token)

const me = await trpcNative.user.me.query()
useAppStore.getState().updateUser(me)

authService.login(responseJson.token)
setLoading(false)
}
} catch (error) {
Expand All @@ -71,20 +74,20 @@ export function useAuth(): [AuthState, AuthApi] {
flow: "auth-code",
})

const api = useMemo(
return useMemo(
() => ({
login() {
setLoading(true)
googleLogin()
},
rename() {
useAppStore.getState().newModal({
useStore.getState().newModal({
modal: function RenameModal() {
return (
<ModalRenameAccount
open={true}
onFinished={() => {
useAppStore.getState().removeModal()
useStore.getState().removeModal()
setLoading(false)
}}
/>
Expand All @@ -97,14 +100,4 @@ export function useAuth(): [AuthState, AuthApi] {
}),
[googleLogin],
)

if ((jwt && !me) || loading || hasHydrated === false) {
return [AuthState.Pending, api]
}

if (me) {
return [AuthState.Authenticated, api]
}

return [AuthState.Unauthenticated, api]
}
21 changes: 13 additions & 8 deletions packages/web/src/common/services/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,8 @@ class AuthService {
return "jwt" in this.state ? this.state.jwt : undefined
}

isAuthenticated() {
return this.state.type === "authenticated"
}

isOffline() {
return this.state.type === "offline"
getState() {
return this.state.type
}

logout() {
Expand All @@ -100,9 +96,18 @@ class AuthService {
useStore.getState().setCurrentUser()
}

login() {}
async login(jwt: string) {
if (this.state.type !== "unauthenticated") {
throw new Error("Tried to login in invalid state")
}

signin() {}
this.state = {
type: "fetching",
jwt,
}

await this.fetchUserInfo()
}

private async fetchUserInfo() {
if (this.state.type !== "fetching") {
Expand Down
Loading

0 comments on commit 329ce24

Please sign in to comment.