2026-07-13 03:48:45 +00:00
import { Router , Response } from 'express' ;
import bcrypt from 'bcryptjs' ;
import jwt from 'jsonwebtoken' ;
import { users , refreshTokens , resetTokens } from '../db' ;
import { requireAuth } from '../middleware/requireAuth' ;
import { EmailService } from '../email/EmailService' ;
2026-07-13 10:33:39 +00:00
import { checkForgotPasswordRate , checkLoginRate , checkRegisterRate , sleep } from '../rateLimiter' ;
2026-07-13 03:48:45 +00:00
import { isSuperAdmin } from '../roles' ;
const router = Router ( ) ;
2026-07-17 11:36:03 +00:00
const ACCESS_TOKEN_TTL = process . env . ACCESS_TOKEN_TTL ? ? '1h' ;
2026-07-13 10:33:39 +00:00
const COOKIE_DOMAIN = process . env . COOKIE_DOMAIN ? ? '.rokojori.com' ;
const RESET_BASE_URL = process . env . RESET_BASE_URL ? ? 'https://account.rokojori.com' ;
const ACCOUNT_BASE_URL = process . env . RESET_BASE_URL ? ? 'https://account.rokojori.com' ;
2026-07-13 03:48:45 +00:00
function issueAccessToken ( userId : string ) : string
{
const user = users . findById ( userId ) ! ;
const payload =
{
userId : user.id ,
email : user.email ,
roles : user.roles ,
products : user.products.map ( p = > p . id ) ,
settings : user.settings
} ;
return jwt . sign ( payload , process . env . JWT_SECRET ? ? '' , { expiresIn : ACCESS_TOKEN_TTL } ) ;
}
2026-07-13 10:33:39 +00:00
function cookieOptions ( maxAge : number )
2026-07-13 03:48:45 +00:00
{
2026-07-13 10:33:39 +00:00
return {
2026-07-13 03:48:45 +00:00
domain : COOKIE_DOMAIN ,
httpOnly : true ,
secure : process.env.NODE_ENV === 'production' ,
2026-07-13 10:33:39 +00:00
sameSite : 'lax' as const ,
2026-07-13 03:48:45 +00:00
path : '/'
2026-07-13 10:33:39 +00:00
} ;
}
function setTokenCookie ( res : Response , accessToken : string ) : void
{
res . cookie ( 'accessToken' , accessToken , { . . . cookieOptions ( 60 * 60 * 1000 ) , maxAge : 60 * 60 * 1000 } ) ;
}
function setRefreshTokenCookie ( res : Response , token : string ) : void
{
res . cookie ( 'refreshToken' , token , { . . . cookieOptions ( 30 * 24 * 60 * 60 * 1000 ) , maxAge : 30 * 24 * 60 * 60 * 1000 } ) ;
}
function clearAuthCookies ( res : Response ) : void
{
const base = { domain : COOKIE_DOMAIN , path : '/' } ;
res . clearCookie ( 'accessToken' , base ) ;
res . clearCookie ( 'refreshToken' , base ) ;
}
function issueTokenPair ( res : Response , userId : string ) : { accessToken : string ; refreshToken : string }
{
const accessToken = issueAccessToken ( userId ) ;
const refreshRecord = refreshTokens . create ( userId ) ;
setTokenCookie ( res , accessToken ) ;
setRefreshTokenCookie ( res , refreshRecord . token ) ;
return { accessToken , refreshToken : refreshRecord.token } ;
2026-07-13 03:48:45 +00:00
}
// POST /api/auth/register
router . post ( '/register' , async ( req , res ) = >
{
2026-07-13 10:33:39 +00:00
const ip = req . ip ? ? 'unknown' ;
const { blocked } = checkRegisterRate ( ip ) ;
if ( blocked ) { res . status ( 429 ) . json ( { error : 'Too many registrations. Try again later.' } ) ; return ; }
2026-07-13 03:48:45 +00:00
const { email , password } = req . body as { email? : string ; password? : string } ;
if ( ! email || ! password )
{
res . status ( 400 ) . json ( { error : 'Email and password required' } ) ;
return ;
}
try
{
const passwordHash = await bcrypt . hash ( password , 10 ) ;
const user = users . create ( email , passwordHash ) ;
const superadminEmail = process . env . INITIAL_SUPERADMIN_EMAIL ? . toLowerCase ( ) ;
if ( superadminEmail && email . toLowerCase ( ) === superadminEmail )
{
const alreadyHasSuperAdmin = users . all ( ) . some ( u = > u . id !== user . id && isSuperAdmin ( u . roles ) ) ;
if ( ! alreadyHasSuperAdmin )
users . update ( user . id , { roles : [ 'superadmin' , 'user' ] } ) ;
}
2026-07-13 10:33:39 +00:00
const tokens = issueTokenPair ( res , user . id ) ;
// Welcome email — non-blocking
EmailService . sendEmail (
email ,
'Welcome to rokojori' ,
` Hi, \ n \ nYour account has been created at ${ ACCOUNT_BASE_URL } . \ n \ nIf you did not create this account or want to delete it, visit: \ n ${ ACCOUNT_BASE_URL } /profile.html \ n \ nYou can delete your account there at any time. `
) . catch ( ( ) = > { } ) ;
res . json ( tokens ) ;
2026-07-13 03:48:45 +00:00
}
catch
{
res . status ( 409 ) . json ( { error : 'Email already registered' } ) ;
}
} ) ;
// POST /api/auth/login
router . post ( '/login' , async ( req , res ) = >
{
2026-07-13 10:33:39 +00:00
const ip = req . ip ? ? 'unknown' ;
const { blocked , delay } = checkLoginRate ( ip ) ;
if ( blocked ) { res . status ( 429 ) . json ( { error : 'Too many attempts. Try again later.' } ) ; return ; }
if ( delay ) await sleep ( delay ) ;
2026-07-13 03:48:45 +00:00
const { email , password } = req . body as { email? : string ; password? : string } ;
const user = email ? users . findByEmail ( email ) : undefined ;
if ( ! user || ! password || ! ( await bcrypt . compare ( password , user . passwordHash ) ) )
{
res . status ( 401 ) . json ( { error : 'Invalid credentials' } ) ;
return ;
}
2026-07-13 10:33:39 +00:00
res . json ( issueTokenPair ( res , user . id ) ) ;
2026-07-13 03:48:45 +00:00
} ) ;
// POST /api/auth/logout
router . post ( '/logout' , ( req , res ) = >
{
2026-07-13 10:33:39 +00:00
const tokenFromBody = ( req . body as { refreshToken? : string } ) . refreshToken ;
const tokenFromCookie = req . cookies ? . refreshToken as string | undefined ;
const token = tokenFromBody ? ? tokenFromCookie ;
if ( token ) refreshTokens . delete ( token ) ;
clearAuthCookies ( res ) ;
2026-07-13 03:48:45 +00:00
res . json ( { ok : true } ) ;
} ) ;
2026-07-13 12:45:19 +00:00
// GET /api/auth/logout?redirect=... — browser clients (link/redirect-based logout)
router . get ( '/logout' , ( req , res ) = >
{
const redirectTo = req . query . redirect as string | undefined ;
const token = req . cookies ? . refreshToken as string | undefined ;
if ( token ) refreshTokens . delete ( token ) ;
clearAuthCookies ( res ) ;
res . redirect ( redirectTo ? ? '/login.html' ) ;
} ) ;
2026-07-13 10:33:39 +00:00
// POST /api/auth/refresh — non-browser clients (token in body)
2026-07-13 03:48:45 +00:00
router . post ( '/refresh' , ( req , res ) = >
{
const { refreshToken } = req . body as { refreshToken? : string } ;
2026-07-13 10:33:39 +00:00
if ( ! refreshToken ) { res . status ( 400 ) . json ( { error : 'Refresh token required' } ) ; return ; }
2026-07-13 03:48:45 +00:00
const record = refreshTokens . find ( refreshToken ) ;
if ( ! record || new Date ( record . expiresAt ) < new Date ( ) )
{
res . status ( 401 ) . json ( { error : 'Invalid or expired refresh token' } ) ;
return ;
}
const user = users . findById ( record . userId ) ;
2026-07-13 10:33:39 +00:00
if ( ! user ) { res . status ( 401 ) . json ( { error : 'User not found' } ) ; return ; }
refreshTokens . delete ( refreshToken ) ;
res . json ( issueTokenPair ( res , user . id ) ) ;
} ) ;
// GET /api/auth/refresh-session?redirect=... — browser clients (token from cookie)
router . get ( '/refresh-session' , ( req , res ) = >
{
const redirectTo = req . query . redirect as string | undefined ;
const loginFallback = redirectTo
? ` /login.html?redirect= ${ encodeURIComponent ( redirectTo ) } `
: '/login.html' ;
const token = req . cookies ? . refreshToken as string | undefined ;
if ( ! token ) { res . redirect ( loginFallback ) ; return ; }
const record = refreshTokens . find ( token ) ;
if ( ! record || new Date ( record . expiresAt ) < new Date ( ) )
2026-07-13 03:48:45 +00:00
{
2026-07-13 10:33:39 +00:00
clearAuthCookies ( res ) ;
res . redirect ( loginFallback ) ;
2026-07-13 03:48:45 +00:00
return ;
}
2026-07-13 10:33:39 +00:00
const user = users . findById ( record . userId ) ;
if ( ! user ) { clearAuthCookies ( res ) ; res . redirect ( loginFallback ) ; return ; }
refreshTokens . delete ( token ) ;
issueTokenPair ( res , user . id ) ;
res . redirect ( redirectTo ? ? '/profile.html' ) ;
2026-07-13 03:48:45 +00:00
} ) ;
// POST /api/auth/forgot-password — rate-limited with escalating delay
router . post ( '/forgot-password' , async ( req , res ) = >
{
const ip = req . ip ? ? 'unknown' ;
const { blocked , delay } = checkForgotPasswordRate ( ip ) ;
2026-07-13 10:33:39 +00:00
if ( blocked ) { res . status ( 429 ) . json ( { error : 'Too many attempts. Try again later.' } ) ; return ; }
2026-07-13 03:48:45 +00:00
await sleep ( delay ) ;
const { email } = req . body as { email? : string } ;
if ( ! email ) { res . status ( 400 ) . json ( { error : 'Email required' } ) ; return ; }
const user = users . findByEmail ( email ) ;
if ( user )
{
const token = resetTokens . create ( user . id ) ;
const link = ` ${ RESET_BASE_URL } /reset-password.html?token= ${ token . token } ` ;
await EmailService . sendEmail (
email ,
'Reset your password — rokojori' ,
` Hi, \ n \ nClick the link below to reset your password. It expires in 1 hour. \ n \ n ${ link } \ n \ nIf you did not request this, you can safely ignore this email. `
) ;
}
res . json ( { ok : true } ) ;
} ) ;
// POST /api/auth/reset-password
router . post ( '/reset-password' , async ( req , res ) = >
{
const { token , password } = req . body as { token? : string ; password? : string } ;
2026-07-13 10:33:39 +00:00
if ( ! token || ! password ) { res . status ( 400 ) . json ( { error : 'Token and password required' } ) ; return ; }
2026-07-13 03:48:45 +00:00
const record = resetTokens . find ( token ) ;
if ( ! record || new Date ( record . expiresAt ) < new Date ( ) )
{
res . status ( 400 ) . json ( { error : 'Invalid or expired token' } ) ;
return ;
}
const passwordHash = await bcrypt . hash ( password , 10 ) ;
users . update ( record . userId , { passwordHash } ) ;
resetTokens . delete ( token ) ;
res . json ( { ok : true } ) ;
} ) ;
2026-07-13 10:33:39 +00:00
// GET /api/auth/reset-token-email
2026-07-13 03:48:45 +00:00
router . get ( '/reset-token-email' , ( req , res ) = >
{
const token = req . query . token as string | undefined ;
const record = token ? resetTokens . find ( token ) : undefined ;
if ( ! record || new Date ( record . expiresAt ) < new Date ( ) )
{
res . status ( 400 ) . json ( { error : 'Invalid or expired token' } ) ;
return ;
}
const user = users . findById ( record . userId ) ;
if ( ! user ) { res . status ( 400 ) . json ( { error : 'User not found' } ) ; return ; }
res . json ( { email : user.email } ) ;
} ) ;
2026-07-15 05:06:39 +00:00
// POST /api/auth/lookup-email — server-to-server; requires SERVICE_SECRET
router . post ( '/lookup-email' , ( req , res ) = >
{
const secret = process . env . SERVICE_SECRET ;
const auth = req . headers . authorization ;
if ( ! secret || auth !== ` Bearer ${ secret } ` )
{
res . status ( 401 ) . json ( { error : 'Unauthorized' } ) ;
return ;
}
const { email } = req . body as { email? : string } ;
if ( ! email ) { res . status ( 400 ) . json ( { error : 'Email required' } ) ; return ; }
const user = users . findByEmail ( email ) ;
if ( ! user ) { res . status ( 404 ) . json ( { error : 'No account found for that email' } ) ; return ; }
res . json ( { id : user.id , email : user.email } ) ;
} ) ;
2026-07-13 03:48:45 +00:00
// GET /api/auth/me
router . get ( '/me' , requireAuth , ( req , res ) = >
{
const user = users . findById ( req . auth ! . userId ) ;
if ( ! user ) { res . status ( 404 ) . json ( { error : 'User not found' } ) ; return ; }
res . json (
{
id : user.id ,
email : user.email ,
roles : user.roles ,
products : user.products ,
settings : user.settings
} ) ;
} ) ;
// PATCH /api/auth/me/settings
router . patch ( '/me/settings' , requireAuth , ( req , res ) = >
{
const user = users . findById ( req . auth ! . userId ) ;
if ( ! user ) { res . status ( 404 ) . json ( { error : 'User not found' } ) ; return ; }
const settings = { . . . user . settings , . . . ( req . body as Record < string , unknown > ) } ;
users . update ( req . auth ! . userId , { settings } ) ;
res . json ( { settings } ) ;
} ) ;
2026-07-13 10:33:39 +00:00
// POST /api/auth/me/password
2026-07-13 03:48:45 +00:00
router . post ( '/me/password' , requireAuth , async ( req , res ) = >
{
const { currentPassword , newPassword } = req . body as { currentPassword? : string ; newPassword? : string } ;
if ( ! currentPassword || ! newPassword )
{
res . status ( 400 ) . json ( { error : 'Current and new password required' } ) ;
return ;
}
const user = users . findById ( req . auth ! . userId ) ;
if ( ! user ) { res . status ( 404 ) . json ( { error : 'User not found' } ) ; return ; }
if ( ! ( await bcrypt . compare ( currentPassword , user . passwordHash ) ) )
{
res . status ( 401 ) . json ( { error : 'Current password is incorrect' } ) ;
return ;
}
const passwordHash = await bcrypt . hash ( newPassword , 10 ) ;
users . update ( user . id , { passwordHash } ) ;
res . json ( { ok : true } ) ;
} ) ;
2026-07-13 10:33:39 +00:00
// DELETE /api/auth/me
router . delete ( '/me' , requireAuth , ( req , res ) = >
{
const userId = req . auth ! . userId ;
refreshTokens . deleteForUser ( userId ) ;
resetTokens . deleteForUser ( userId ) ;
users . delete ( userId ) ;
clearAuthCookies ( res ) ;
res . json ( { ok : true } ) ;
} ) ;
2026-07-13 03:48:45 +00:00
export default router ;