11import { createServer } from 'node:http' ;
2+ import { createHmac , timingSafeEqual } from 'node:crypto' ;
23import { URL } from 'node:url' ;
34import { database , dbPath } from './db.js' ;
4- require ( "./env" ) ;
5+ import "./env.js" ;
56
67const port = Number ( process . env . PORT || 4000 ) ;
78
89const LOGIN_RATE_LIMIT_MAX_ATTEMPTS = Number ( process . env . LOGIN_RATE_LIMIT_MAX_ATTEMPTS || 5 ) ;
910const LOGIN_RATE_LIMIT_WINDOW_MS = Number ( process . env . LOGIN_RATE_LIMIT_WINDOW_MS || 15 * 60 * 1000 ) ;
1011const LOGIN_RATE_LIMIT_BLOCK_MS = Number ( process . env . LOGIN_RATE_LIMIT_BLOCK_MS || 15 * 60 * 1000 ) ;
12+ const AUTH_TOKEN_SECRET = process . env . AUTH_TOKEN_SECRET || 'brocode-dev-secret-change-me' ;
13+ const AUTH_TOKEN_TTL_SECONDS = Number ( process . env . AUTH_TOKEN_TTL_SECONDS || 60 * 60 * 12 ) ;
14+ const CORS_ALLOW_ORIGIN = process . env . CORS_ALLOW_ORIGIN || '*' ;
1115const loginAttempts = new Map ( ) ;
1216
1317const getLoginKey = ( req , username ) => {
@@ -62,18 +66,64 @@ const parseBearerToken = (authHeader) => {
6266 return token ;
6367} ;
6468
69+ const toBase64Url = ( value ) => Buffer . from ( value ) . toString ( 'base64url' ) ;
70+
71+ const signToken = ( payload ) =>
72+ createHmac ( 'sha256' , AUTH_TOKEN_SECRET ) . update ( payload ) . digest ( 'base64url' ) ;
73+
74+ const generateAuthToken = ( user ) => {
75+ const payload = {
76+ sub : user . id ,
77+ role : user . role ,
78+ exp : Math . floor ( Date . now ( ) / 1000 ) + AUTH_TOKEN_TTL_SECONDS ,
79+ } ;
80+
81+ const payloadPart = toBase64Url ( JSON . stringify ( payload ) ) ;
82+ const signature = signToken ( payloadPart ) ;
83+ return `${ payloadPart } .${ signature } ` ;
84+ } ;
85+
86+ const verifyAuthToken = ( token ) => {
87+ const [ payloadPart , signaturePart ] = token . split ( '.' ) ;
88+ if ( ! payloadPart || ! signaturePart ) {
89+ return null ;
90+ }
91+
92+ const expectedSignature = signToken ( payloadPart ) ;
93+ const providedSignatureBuffer = Buffer . from ( signaturePart , 'base64url' ) ;
94+ const expectedSignatureBuffer = Buffer . from ( expectedSignature , 'base64url' ) ;
95+ if ( providedSignatureBuffer . length !== expectedSignatureBuffer . length ) {
96+ return null ;
97+ }
98+
99+ if ( ! timingSafeEqual ( providedSignatureBuffer , expectedSignatureBuffer ) ) {
100+ return null ;
101+ }
102+
103+ try {
104+ const payload = JSON . parse ( Buffer . from ( payloadPart , 'base64url' ) . toString ( 'utf-8' ) ) ;
105+ if ( ! payload . sub || ! payload . exp || payload . exp < Math . floor ( Date . now ( ) / 1000 ) ) {
106+ return null ;
107+ }
108+
109+ return payload ;
110+ } catch {
111+ return null ;
112+ }
113+ } ;
114+
65115const getUserFromAuthHeader = ( authHeader ) => {
66116 const token = parseBearerToken ( authHeader ) ;
67- if ( ! token || ! token . startsWith ( 'demo-token-' ) ) {
117+ if ( ! token ) {
68118 return null ;
69119 }
70120
71- const userId = token . slice ( 'demo- token-' . length ) ;
72- if ( ! userId ) {
121+ const verifiedPayload = verifyAuthToken ( token ) ;
122+ if ( ! verifiedPayload ) {
73123 return null ;
74124 }
75125
76- return database . getUserById ( userId ) ;
126+ return database . getUserById ( verifiedPayload . sub ) ;
77127} ;
78128
79129const recordFailedLoginAttempt = ( key ) => {
@@ -89,8 +139,8 @@ const recordFailedLoginAttempt = (key) => {
89139const sendJson = ( res , statusCode , body ) => {
90140 res . writeHead ( statusCode , {
91141 'Content-Type' : 'application/json' ,
92- 'Access-Control-Allow-Origin' : '*' ,
93- 'Access-Control-Allow-Headers' : 'Content-Type' ,
142+ 'Access-Control-Allow-Origin' : CORS_ALLOW_ORIGIN ,
143+ 'Access-Control-Allow-Headers' : 'Content-Type, Authorization ' ,
94144 'Access-Control-Allow-Methods' : 'GET,POST,DELETE,OPTIONS' ,
95145 } ) ;
96146 res . end ( JSON . stringify ( body ) ) ;
@@ -166,7 +216,7 @@ const server = createServer(async (req, res) => {
166216
167217 clearRateLimitState ( loginKey ) ;
168218
169- sendJson ( res , 200 , { token : `demo-token- ${ user . id } ` , user } ) ;
219+ sendJson ( res , 200 , { token : generateAuthToken ( user ) , user } ) ;
170220 return ;
171221 } catch ( error ) {
172222 sendJson ( res , 400 , { error : error . message } ) ;
@@ -199,10 +249,23 @@ const server = createServer(async (req, res) => {
199249 }
200250
201251 if ( method === 'GET' && path === '/api/orders' ) {
252+ const authedUser = getUserFromAuthHeader ( req . headers . authorization ) ;
253+ if ( ! authedUser ) {
254+ sendJson ( res , 401 , { error : 'Unauthorized' } ) ;
255+ return ;
256+ }
257+
202258 const spotId = parsedUrl . searchParams . get ( 'spotId' ) ;
203259 const userId = parsedUrl . searchParams . get ( 'userId' ) ;
204260
205- const orders = database . getOrders ( { spotId, userId } ) ;
261+ if ( authedUser . role !== 'admin' && userId && userId !== authedUser . id ) {
262+ sendJson ( res , 403 , { error : 'Forbidden' } ) ;
263+ return ;
264+ }
265+
266+ const effectiveUserId = authedUser . role === 'admin' ? userId : authedUser . id ;
267+
268+ const orders = database . getOrders ( { spotId, userId : effectiveUserId } ) ;
206269 sendJson ( res , 200 , orders ) ;
207270 return ;
208271 }
@@ -234,13 +297,24 @@ const server = createServer(async (req, res) => {
234297
235298 if ( method === 'POST' && path === '/api/orders' ) {
236299 try {
300+ const authedUser = getUserFromAuthHeader ( req . headers . authorization ) ;
301+ if ( ! authedUser ) {
302+ sendJson ( res , 401 , { error : 'Unauthorized' } ) ;
303+ return ;
304+ }
305+
237306 const { spotId, userId, items } = await readBody ( req ) ;
238307
239308 if ( ! spotId || ! userId || ! Array . isArray ( items ) || items . length === 0 ) {
240309 sendJson ( res , 400 , { error : 'spotId, userId and at least one order item are required' } ) ;
241310 return ;
242311 }
243312
313+ if ( authedUser . role !== 'admin' && userId !== authedUser . id ) {
314+ sendJson ( res , 403 , { error : 'Forbidden' } ) ;
315+ return ;
316+ }
317+
244318 if ( ! database . userExists ( userId ) ) {
245319 sendJson ( res , 404 , { error : `Unknown userId: ${ userId } ` } ) ;
246320 return ;
@@ -265,13 +339,25 @@ const server = createServer(async (req, res) => {
265339 }
266340
267341 if ( method === 'GET' && path . startsWith ( '/api/bills/' ) ) {
342+ const authedUser = getUserFromAuthHeader ( req . headers . authorization ) ;
343+ if ( ! authedUser || authedUser . role !== 'admin' ) {
344+ sendJson ( res , 403 , { error : 'Forbidden' } ) ;
345+ return ;
346+ }
347+
268348 const spotId = path . replace ( '/api/bills/' , '' ) ;
269349 const bill = database . getBillBySpotId ( spotId ) ;
270350 sendJson ( res , 200 , bill ) ;
271351 return ;
272352 }
273353
274354 if ( method === 'DELETE' && path . startsWith ( '/api/users/' ) ) {
355+ const authedUser = getUserFromAuthHeader ( req . headers . authorization ) ;
356+ if ( ! authedUser || authedUser . role !== 'admin' ) {
357+ sendJson ( res , 403 , { error : 'Forbidden' } ) ;
358+ return ;
359+ }
360+
275361 const userId = path . replace ( '/api/users/' , '' ) ;
276362
277363 if ( ! userId ) {
0 commit comments