create-cf-planetscale-app
Version:
Create a Cloudflare workers app for building production ready RESTful APIs using Hono
166 lines (157 loc) • 4.66 kB
text/typescript
import jwt, { JwtPayload } from '@tsndr/cloudflare-worker-jwt'
import dayjs, { Dayjs } from 'dayjs'
import httpStatus from 'http-status'
import { Selectable } from 'kysely'
import { Config } from '../config/config'
import { getDBClient } from '../config/database'
import { Role } from '../config/roles'
import { TokenType, tokenTypes } from '../config/tokens'
import { OneTimeOauthCode } from '../models/one-time-oauth-code'
import { TokenResponse } from '../models/token.model'
import { User } from '../models/user.model'
import { ApiError } from '../utils/api-error'
import { generateId } from '../utils/utils'
export const generateToken = async (
userId: string,
type: TokenType,
role: Role,
expires: Dayjs,
secret: string,
isEmailVerified: boolean
) => {
const payload = {
sub: userId.toString(),
exp: expires.unix(),
iat: dayjs().unix(),
type,
role,
isEmailVerified
}
return jwt.sign(payload, secret)
}
export const generateAuthTokens = async (user: Selectable<User>, jwtConfig: Config['jwt']) => {
const accessTokenExpires = dayjs().add(jwtConfig.accessExpirationMinutes, 'minutes')
const accessToken = await generateToken(
user.id,
tokenTypes.ACCESS,
user.role,
accessTokenExpires,
jwtConfig.secret,
user.is_email_verified
)
const refreshTokenExpires = dayjs().add(jwtConfig.refreshExpirationDays, 'days')
const refreshToken = await generateToken(
user.id,
tokenTypes.REFRESH,
user.role,
refreshTokenExpires,
jwtConfig.secret,
user.is_email_verified
)
return {
access: {
token: accessToken,
expires: accessTokenExpires.toDate()
},
refresh: {
token: refreshToken,
expires: refreshTokenExpires.toDate()
}
}
}
export const verifyToken = async (token: string, type: TokenType, secret: string) => {
const isValid = await jwt.verify(token, secret)
if (!isValid) {
throw new Error('Token not valid')
}
const decoded = jwt.decode(token)
const payload = decoded.payload as JwtPayload
if (type !== payload.type) {
throw new Error('Token not valid')
}
return payload
}
export const generateVerifyEmailToken = async (
user: Selectable<User>,
jwtConfig: Config['jwt']
) => {
const expires = dayjs().add(jwtConfig.verifyEmailExpirationMinutes, 'minutes')
const verifyEmailToken = await generateToken(
user.id,
tokenTypes.VERIFY_EMAIL,
user.role,
expires,
jwtConfig.secret,
user.is_email_verified
)
return verifyEmailToken
}
export const generateResetPasswordToken = async (
user: Selectable<User>,
jwtConfig: Config['jwt']
) => {
const expires = dayjs().add(jwtConfig.resetPasswordExpirationMinutes, 'minutes')
const resetPasswordToken = await generateToken(
user.id,
tokenTypes.RESET_PASSWORD,
user.role,
expires,
jwtConfig.secret,
user.is_email_verified
)
return resetPasswordToken
}
const generateOneTimeOauthCodeToken = () => {
return generateId()
}
export const createOneTimeOauthCode = async (
userId: string,
tokens: TokenResponse,
config: Config
) => {
const db = getDBClient(config.database)
let attempts = 0
const maxAttempts = 5
let code = generateOneTimeOauthCodeToken()
while (attempts < maxAttempts) {
try {
await db
.insertInto('one_time_oauth_code')
.values({
code,
user_id: userId,
access_token: tokens.access.token,
access_token_expires_at: tokens.access.expires,
refresh_token: tokens.refresh.token,
refresh_token_expires_at: tokens.refresh.expires,
expires_at: dayjs().add(config.jwt.accessExpirationMinutes, 'minutes').toDate()
})
.executeTakeFirstOrThrow()
break
} catch {
if (attempts >= maxAttempts) {
throw new ApiError(httpStatus.INTERNAL_SERVER_ERROR, 'Failed to create one time code')
}
code = generateOneTimeOauthCodeToken()
attempts++
}
}
return code
}
export const getOneTimeOauthCode = async (code: string, config: Config) => {
const db = getDBClient(config.database)
const oneTimeCode = await db.transaction().execute(async (trx) => {
const oneTimeCode = await db
.selectFrom('one_time_oauth_code')
.selectAll()
.where('code', '=', code)
.where('expires_at', '>', dayjs().toDate())
.executeTakeFirst()
if (!oneTimeCode) {
throw new ApiError(httpStatus.BAD_REQUEST, 'Code invalid or expired')
}
await trx.deleteFrom('one_time_oauth_code').where('code', '=', code).execute()
return oneTimeCode
})
return new OneTimeOauthCode(oneTimeCode)
}