@@ -3,7 +3,6 @@ import { withDb } from "$db";
33import * as schema from "$drizzle/schema" ;
44import { getServerCookieDomain } from "@/lib/cookies" ;
55import { getCookie , setCookie } from "cookies-next" ;
6- import crypto from "crypto" ;
76import { and , eq } from "drizzle-orm" ;
87import { NextApiRequest , NextApiResponse } from "next" ;
98
@@ -49,80 +48,116 @@ export interface Player {
4948 animals ?: object ;
5049}
5150
51+ function randomHex ( byteCount : number ) : string {
52+ const bytes = new Uint8Array ( byteCount ) ;
53+ crypto . getRandomValues ( bytes ) ;
54+ return Array . from ( bytes )
55+ . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) )
56+ . join ( "" ) ;
57+ }
58+
59+ async function hmacSign ( key : string , data : string ) : Promise < string > {
60+ const encoder = new TextEncoder ( ) ;
61+ const cryptoKey = await crypto . subtle . importKey (
62+ "raw" ,
63+ encoder . encode ( key ) ,
64+ { name : "HMAC" , hash : "SHA-256" } ,
65+ false ,
66+ [ "sign" ] ,
67+ ) ;
68+ const signature = await crypto . subtle . sign (
69+ "HMAC" ,
70+ cryptoKey ,
71+ encoder . encode ( data ) ,
72+ ) ;
73+ return Array . from ( new Uint8Array ( signature ) )
74+ . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) )
75+ . join ( "" ) ;
76+ }
77+
78+ async function hmacVerify (
79+ key : string ,
80+ data : string ,
81+ signature : string ,
82+ ) : Promise < boolean > {
83+ const expected = await hmacSign ( key , data ) ;
84+ return expected === signature ;
85+ }
86+
87+ export interface AuthResult {
88+ uid : string ;
89+ user ?: SqlUser ;
90+ }
91+
5292export async function getUID (
5393 req : NextApiRequest ,
5494 res : NextApiResponse < Data > ,
5595 db : Db ,
56- ) : Promise < string > {
96+ ) : Promise < AuthResult > {
5797 let uid = getCookie ( "uid" , { req, res } ) ;
5898 if ( uid && typeof uid === "string" ) {
59- // uids can be anonymous, so we need to check if the user exists
99+ // Check if user has a token cookie - if not, they're anonymous
100+ const token = getCookie ( "token" , { req, res } ) ;
101+ if ( ! token ) {
102+ return { uid } ;
103+ }
60104
105+ // User has a token, so verify it against the DB
61106 const [ user ] = await db
62107 . select ( )
63108 . from ( schema . users )
64109 . where ( eq ( schema . users . id , uid ) )
65110 . limit ( 1 ) ;
66111
67112 if ( user ) {
68- // user exists, so we check if the user is authenticated
69- // verify that the user has a stored token
70- let token = getCookie ( "token" , { req, res } ) ;
71- if ( ! token ) {
72- res . status ( 400 ) ;
73- throw new Error ( "User is not authenticated (1)" ) ;
74- }
75- // verify that the token is valid
76- const { valid, userId } = verifyToken (
113+ const { valid, userId } = await verifyToken (
77114 token as string ,
78115 user . cookie_secret ,
79116 ) ;
80117 if ( ! valid || userId !== uid ) {
81118 res . status ( 400 ) ;
82- throw new Error ( `User is not authenticated (valid token: ${ valid } )` ) ;
119+ throw new Error (
120+ `User is not authenticated (valid token: ${ valid } )` ,
121+ ) ;
83122 }
123+ return { uid, user : user as SqlUser } ;
84124 }
85- // everything is ok, so we return the uid
86- return uid as string ;
125+
126+ // uid cookie exists but user not in DB - treat as anonymous
127+ return { uid } ;
87128 } else {
88- console . log ( "Generating new UID..." ) ;
89129 // no uid, so we create an anonymous one
90- uid = crypto . randomBytes ( 16 ) . toString ( "hex" ) ;
130+ uid = randomHex ( 16 ) ;
91131 setCookie ( "uid" , uid , {
92132 req,
93133 res,
94134 maxAge : 60 * 60 * 24 * 365 ,
95135 domain : getServerCookieDomain ( req ) ,
96136 } ) ;
97137 }
98- return uid ;
138+ return { uid } ;
99139}
100140
101- // magic functions dreamt up by me, i think they're secure lol, i use them a lot - Leah
102- export const createToken = ( userId : string , key : string , validFor : number ) => {
141+ export const createToken = async (
142+ userId : string ,
143+ key : string ,
144+ validFor : number ,
145+ ) => {
103146 const expires = Math . floor ( new Date ( ) . getTime ( ) / 1000 + validFor ) ;
104- const salt = crypto . randomBytes ( 8 ) . toString ( "hex" ) ;
105- const payload = Buffer . from ( `${ expires } .${ userId } .${ salt } ` , "utf8" ) . toString (
106- "base64" ,
107- ) ;
108- const signature = crypto
109- . createHmac ( "sha256" , key )
110- . update ( payload )
111- . digest ( "hex" ) ;
147+ const salt = randomHex ( 8 ) ;
148+ const payload = btoa ( `${ expires } .${ userId } .${ salt } ` ) ;
149+ const signature = await hmacSign ( key , payload ) ;
112150 return { token : `${ payload } .${ signature } ` , expires } ;
113151} ;
114152
115- export const verifyToken = ( token : string , key : string ) => {
153+ export const verifyToken = async ( token : string , key : string ) => {
116154 const [ payload , signature ] = token . split ( "." ) ;
117- const decoded = Buffer . from ( payload , "base64" ) . toString ( "utf8" ) ;
155+ const decoded = atob ( payload ) ;
118156 const [ expires , userId ] = decoded . split ( "." ) ;
119- const expectedSignature = crypto
120- . createHmac ( "sha256" , key )
121- . update ( payload )
122- . digest ( "hex" ) ;
157+ const valid = await hmacVerify ( key , payload , signature ) ;
123158 return {
124159 valid :
125- signature === expectedSignature &&
160+ valid &&
126161 parseInt ( expires ) > Math . floor ( new Date ( ) . getTime ( ) / 1000 ) ,
127162 userId,
128163 } ;
@@ -139,7 +174,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
139174 "no-store, no-cache, must-revalidate, max-age=0" ,
140175 ) ;
141176
142- const uid = await getUID ( req , res , db ) ;
177+ const { uid } = await getUID ( req , res , db ) ;
143178 const players = await getPlayersByUid ( db , uid ) ;
144179
145180 res . json ( players ) ;
@@ -153,26 +188,29 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
153188 "no-store, no-cache, must-revalidate, max-age=0" ,
154189 ) ;
155190
156- const uid = await getUID ( req , res , db ) ;
191+ const { uid } = await getUID ( req , res , db ) ;
157192 const players = parseRequestBody < Player [ ] > ( req . body ) ;
158193
159194 try {
160- for ( const player of players ) {
161- if ( ! player . _id ) continue ;
162-
163- await db
164- . insert ( schema . saves )
165- . values ( {
166- _id : player . _id ,
167- user_id : uid ,
168- ...player ,
169- } )
170- . onDuplicateKeyUpdate ( {
171- set : {
172- user_id : uid ,
173- ...player ,
174- } ,
175- } ) ;
195+ const validPlayers = players . filter ( ( p ) => p . _id ) ;
196+ if ( validPlayers . length > 0 ) {
197+ await Promise . all (
198+ validPlayers . map ( ( player ) =>
199+ db
200+ . insert ( schema . saves )
201+ . values ( {
202+ _id : player . _id ! ,
203+ user_id : uid ,
204+ ...player ,
205+ } )
206+ . onDuplicateKeyUpdate ( {
207+ set : {
208+ user_id : uid ,
209+ ...player ,
210+ } ,
211+ } ) ,
212+ ) ,
213+ ) ;
176214 }
177215
178216 const savedPlayers = await getPlayersByUid ( db , uid ) ;
@@ -186,7 +224,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
186224
187225async function _delete ( req : NextApiRequest , res : NextApiResponse ) {
188226 return withDb ( async ( db ) => {
189- const uid = await getUID ( req , res , db ) ;
227+ const { uid } = await getUID ( req , res , db ) ;
190228 const body = req . body
191229 ? parseRequestBody < { type ?: string ; _id ?: string } > ( req . body )
192230 : undefined ;
@@ -201,7 +239,10 @@ async function _delete(req: NextApiRequest, res: NextApiResponse) {
201239 await db
202240 . delete ( schema . saves )
203241 . where (
204- and ( eq ( schema . saves . user_id , uid ) , eq ( schema . saves . _id , playerId ) ) ,
242+ and (
243+ eq ( schema . saves . user_id , uid ) ,
244+ eq ( schema . saves . _id , playerId ) ,
245+ ) ,
205246 ) ;
206247 } else if ( type === "account" ) {
207248 await db . delete ( schema . saves ) . where ( eq ( schema . saves . user_id , uid ) ) ;
0 commit comments