2025-05-07 15:47:08 +01:00
import { DEMO_USER , type User } from "@markone/core/user"
2025-05-06 11:00:35 +01:00
import { type } from "arktype"
import dayjs from "dayjs"
import { ulid } from "ulid"
import { db } from "~/database.ts"
import { HttpError } from "~/error.ts"
import { httpHandler } from "~/http-handler.ts"
import { createUser , findUserById , findUserByUsername } from "~/user/user.ts"
2025-05-07 15:47:08 +01:00
import { hashPassword , verifyPassword } from "./password.ts"
2025-05-06 11:00:35 +01:00
import { createSessionForUser , extendSession , forgetAllSessions , saveSession , verifySession } from "./session.ts"
const SignUpRequest = type ( {
username : "string" ,
password : "string" ,
} )
const LoginRequest = type ( {
username : "string" ,
password : "string" ,
} )
function authenticated < Route extends string > (
handler : ( request : Bun.BunRequest < Route > , user : User ) = > Promise < Response > ,
) {
return httpHandler < Route > ( ( request ) = > {
const session = verifySession ( request . cookies )
if ( ! session ) {
2025-05-07 15:47:08 +01:00
console . log ( "session not found!" )
2025-05-06 11:00:35 +01:00
throw new HttpError ( 401 )
}
const user = findUserById ( session . userId )
if ( ! user ) {
throw new HttpError ( 401 )
}
const authTokenCookie = request . cookies . get ( "auth-token" )
if ( authTokenCookie ) {
2025-05-07 15:47:08 +01:00
const deleteAuthTokenQuery = db . query ( "DELETE FROM auth_tokens WHERE id = $id" )
2025-05-06 11:00:35 +01:00
// biome-ignore lint/style/noNonNullAssertion: the cookie has already been verified by verifySession previously, therefore the cookie must be in the correct format <token-id>:<token-signature>
const tokenId = authTokenCookie . split ( ":" ) [ 0 ] !
deleteAuthTokenQuery . run ( { id : tokenId } )
rememberLoginForUser ( user , request . cookies )
}
if ( user . id !== DEMO_USER . id ) {
const extendedSession = extendSession ( session )
saveSession ( extendedSession , request . cookies )
}
return handler ( request , user )
} )
}
function rememberLoginForUser ( user : User , cookies : Bun.CookieMap ) {
const tokenId = ulid ( )
const authToken = Buffer . alloc ( 32 )
crypto . getRandomValues ( authToken )
const hasher = new Bun . CryptoHasher ( "sha256" )
hasher . update ( authToken )
const hashedToken = hasher . digest ( "base64url" )
const expiryDate = dayjs ( ) . add ( 1 , "month" )
2025-05-07 15:47:08 +01:00
const createAuthTokenQuery = db . query (
"INSERT INTO auth_tokens(id, token, user_id, expires_at_unix_ms) VALUES ($id, $token, $userId, $expiresAt)" ,
)
2025-05-06 11:00:35 +01:00
createAuthTokenQuery . run ( {
id : tokenId ,
token : hashedToken ,
userId : user.id ,
expiresAt : expiryDate.valueOf ( ) ,
} )
cookies . set ( "auth-token" , ` ${ tokenId } : ${ authToken . toBase64 ( { alphabet : "base64url" } )} ` , {
maxAge : 30 * 24 * 60 * 60 * 1000 ,
2025-05-07 15:47:08 +01:00
path : "/api" ,
2025-05-06 11:00:35 +01:00
httpOnly : true ,
} )
}
async function signUp ( request : Bun.BunRequest < "/api/sign-up" > ) {
const body = await request . json ( ) . catch ( ( ) = > {
throw new HttpError ( 500 )
} )
const signUpRequest = SignUpRequest ( body )
if ( signUpRequest instanceof type . errors ) {
2025-05-07 23:09:14 +01:00
throw new HttpError ( 400 , "BadRequestBody" , signUpRequest . summary )
2025-05-06 11:00:35 +01:00
}
const { username , password } = signUpRequest
2025-05-07 15:47:08 +01:00
const hashedPassword = await hashPassword ( password )
2025-05-06 11:00:35 +01:00
const user = createUser ( username , hashedPassword )
await createSessionForUser ( user , request . cookies )
rememberLoginForUser ( user , request . cookies )
return Response . json ( user , { status : 200 } )
}
async function login ( request : Bun.BunRequest < "/api/login" > ) {
const body = await request . json ( ) . catch ( ( ) = > {
throw new HttpError ( 500 )
} )
const loginRequest = LoginRequest ( body )
if ( loginRequest instanceof type . errors ) {
2025-05-07 23:09:14 +01:00
throw new HttpError ( 400 , "BadRequestBody" )
2025-05-06 11:00:35 +01:00
}
const foundUser = findUserByUsername ( loginRequest . username , {
password : true ,
} )
if ( ! foundUser ) {
2025-05-07 15:47:08 +01:00
throw new HttpError ( 401 )
2025-05-06 11:00:35 +01:00
}
2025-05-07 15:47:08 +01:00
const ok = await verifyPassword ( loginRequest . password , foundUser . password ) . catch ( ( ) = > {
2025-05-06 11:00:35 +01:00
throw new HttpError ( 401 )
} )
if ( ! ok ) {
throw new HttpError ( 401 )
}
const user : User = {
id : foundUser.id ,
username : foundUser.username ,
}
2025-05-07 15:47:08 +01:00
await createSessionForUser ( user , request . cookies )
if ( user . id !== DEMO_USER . id ) {
2025-05-06 11:00:35 +01:00
rememberLoginForUser ( user , request . cookies )
}
return Response . json ( user , { status : 200 } )
}
async function logout ( request : Bun.BunRequest < "/api/logout" > , user : User ) : Promise < Response > {
2025-05-07 15:47:08 +01:00
const deleteAllAuthTokensQuery = db . query ( "DELETE FROM auth_tokens WHERE user_id = $userId" )
2025-05-06 11:00:35 +01:00
forgetAllSessions ( user )
deleteAllAuthTokensQuery . run ( { userId : user.id } )
2025-05-07 23:09:14 +01:00
return new Response ( undefined , { status : 204 } )
2025-05-06 11:00:35 +01:00
}
export { authenticated , signUp , login , logout }