Skip to content

Commit

Permalink
Allow enable user, disable user, send verify email through s2s api (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
byn9826 authored Jul 19, 2024
1 parent e755faf commit fe6fa61
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 27 deletions.
9 changes: 9 additions & 0 deletions server/src/configs/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export class Forbidden {
}
}

export class NotFound {
constructor (message?: string) {
return new HTTPException(
400,
getOption(message),
)
}
}

export class UnAuthorized {
constructor (message?: string) {
return new HTTPException(
Expand Down
2 changes: 2 additions & 0 deletions server/src/configs/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export enum Error {
EmailTaken = 'The email address is already in use.',
WrongRedirectUri = 'Invalid redirect_uri',
NoUser = 'No user found',
UserDisabled = 'This account has been disabled',
EmailAlreadyVerified = 'Email already verified',
NoConsent = 'User consent required',
CanNotCreateUser = 'Failed to create user',
CanNotCreateConsent = 'Failed to create consent',
Expand Down
1 change: 1 addition & 0 deletions server/src/configs/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum Scope {
Profile = 'profile',
OfflineAccess = 'offline_access',
READ_USER = 'read_user',
WRITE_USER = 'write_user',
}

export enum ClientType {
Expand Down
2 changes: 1 addition & 1 deletion server/src/handlers/postTokenReq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const parseClientCredentials = async (c: Context<typeConfig.Context>) =>

const bodyDto = new oauthDto.PostTokenClientCredentialsReqBodyDto({
grantType: String(reqBody.grant_type),
scopes: reqBody.scope ? String(reqBody.scope).split(',') : [],
scopes: reqBody.scope ? String(reqBody.scope).split(' ') : [],
})
await validateUtil.dto(bodyDto)

Expand Down
23 changes: 23 additions & 0 deletions server/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ export const s2sReadUser = bearerAuth({
},
})

export const s2sWriteUser = bearerAuth({
verifyToken: async (
token, c: Context<typeConfig.Context>,
) => {
const accessTokenBody = await parseToken(
c,
token,
typeConfig.ClientType.S2S,
)
if (!accessTokenBody) return false

const scopes = accessTokenBody.scope?.split(' ') ?? []
if (!scopes.includes(typeConfig.Scope.WRITE_USER)) return false

c.set(
'access_token_body',
accessTokenBody,
)

return true
},
})

export const s2sBasicAuth = async (
c: Context<typeConfig.Context>, next: Next,
) => {
Expand Down
13 changes: 10 additions & 3 deletions server/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ export interface Update {

const TableName = dbConfig.TableName.User

export const getAll = async (db: D1Database) => {
const stmt = db.prepare(`SELECT * FROM ${TableName} WHERE deletedAt IS NULL`)
export const getAll = async (
db: D1Database, includeDeleted: boolean = false,
) => {
let query = `SELECT * FROM ${TableName}`
if (!includeDeleted) query = `${query} WHERE deletedAt IS NULL`
const stmt = db.prepare(query)
const { results: users }: { results: Record[] } = await stmt.all()
return users
}
Expand All @@ -60,8 +64,11 @@ export const getById = async (
export const getByAuthId = async (
db: D1Database,
authId: string,
includeDeleted: boolean = false,
) => {
const stmt = db.prepare(`SELECT * FROM ${TableName} WHERE authId = $1 AND deletedAt IS NULL`)
let query = `SELECT * FROM ${TableName} WHERE authId = $1`
if (!includeDeleted) query = `${query} AND deletedAt IS NULL`
const stmt = db.prepare(query)
.bind(authId)
const user = await stmt.first() as Record | null
return user
Expand Down
82 changes: 78 additions & 4 deletions server/src/routes/user.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import {
errorConfig,
localeConfig,
routeConfig, typeConfig,
} from 'configs'
import { authMiddleware } from 'middlewares'
import {
authMiddleware, configMiddleware,
} from 'middlewares'
import { userModel } from 'models'
import { userService } from 'services'
import { timeUtil } from 'utils'

const BaseRoute = routeConfig.InternalRoute.ApiUsers

Expand All @@ -11,7 +17,11 @@ export const load = (app: typeConfig.App) => {
`${BaseRoute}`,
authMiddleware.s2sReadUser,
async (c) => {
const users = await userModel.getAll(c.env.DB)
const includeDeleted = c.req.query('includeDisabled') === 'true'
const users = await userModel.getAll(
c.env.DB,
includeDeleted,
)
return c.json({ users })
},
)
Expand All @@ -21,11 +31,75 @@ export const load = (app: typeConfig.App) => {
authMiddleware.s2sReadUser,
async (c) => {
const authId = c.req.param('authId')
const user = await userModel.getByAuthId(
c.env.DB,
const user = await userService.getUserByAuthId(
c,
authId,
)
return c.json({ user })
},
)

app.post(
`${BaseRoute}/:authId/verify-email`,
authMiddleware.s2sWriteUser,
configMiddleware.enableEmailVerification,
async (c) => {
const authId = c.req.param('authId')
const user = await userService.getUserByAuthId(
c,
authId,
)

if (user.emailVerified) throw new errorConfig.Forbidden(localeConfig.Error.EmailAlreadyVerified)

await userService.sendEmailVerification(
c,
user,
)
return c.json({ success: true })
},
)

app.put(
`${BaseRoute}/:authId/enable`,
authMiddleware.s2sWriteUser,
async (c) => {
const authId = c.req.param('authId')
const includeDeleted = true
const user = await userService.getUserByAuthId(
c,
authId,
includeDeleted,
)

if (!user.deletedAt) throw new errorConfig.NotFound(localeConfig.Error.NoUser)

await userModel.update(
c.env.DB,
user.id,
{ deletedAt: null },
)

return c.json({ success: true })
},
)

app.put(
`${BaseRoute}/:authId/disable`,
authMiddleware.s2sWriteUser,
async (c) => {
const authId = c.req.param('authId')
const user = await userService.getUserByAuthId(
c,
authId,
)

await userModel.update(
c.env.DB,
user.id,
{ deletedAt: timeUtil.getDbCurrentTime() },
)
return c.json({ success: true })
},
)
}
45 changes: 26 additions & 19 deletions server/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ export const getUserInfo = async (
return result
}

export const getUserByAuthId = async (
c: Context<typeConfig.Context>,
authId: string,
includeDeleted: boolean = false,
) => {
const user = await userModel.getByAuthId(
c.env.DB,
authId,
includeDeleted,
)
if (!user) throw new errorConfig.NotFound(localeConfig.Error.NoUser)
return user
}

export const verifyPasswordSignIn = async (
c: Context<typeConfig.Context>, bodyDto: PostAuthorizeReqBodyWithPasswordDto,
) => {
Expand All @@ -70,26 +84,19 @@ export const createAccountWithPassword = async (
bodyDto.email,
includeDeleted,
)
if (user && !user.deletedAt) throw new Forbidden(localeConfig.Error.EmailTaken)

const newUser = user
? await userModel.update(
c.env.DB,
user.id,
{
password, firstName: bodyDto.firstName, lastName: bodyDto.lastName, deletedAt: null,
},
)
: await userModel.create(
c.env.DB,
{
authId: crypto.randomUUID(),
email: bodyDto.email,
password,
firstName: bodyDto.firstName,
lastName: bodyDto.lastName,
},
)
if (user) throw new Forbidden(user.deletedAt ? localeConfig.Error.UserDisabled : localeConfig.Error.EmailTaken)

const newUser = await userModel.create(
c.env.DB,
{
authId: crypto.randomUUID(),
email: bodyDto.email,
password,
firstName: bodyDto.firstName,
lastName: bodyDto.lastName,
},
)

if (!newUser) {
throw new errorConfig.InternalServerError(localeConfig.Error.CanNotCreateUser)
Expand Down

0 comments on commit fe6fa61

Please sign in to comment.