Skip to content

Commit 8cf5e0a

Browse files
feat: add user CRUD API endpoints
This commit implements the following API endpoints for user management: - POST /api/user: Creates a new user (admin only). - GET /api/user/:id: Retrieves a user by ID (admin or self). - PATCH /api/user/:id: Updates a user's information (admin only). - DELETE /api/user/:id: Deletes a user (admin only). New utility functions have been added to support these endpoints, and unit tests have been added to ensure they work correctly.
1 parent 2b42748 commit 8cf5e0a

File tree

6 files changed

+563
-0
lines changed

6 files changed

+563
-0
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createError, defineEventHandler, getCookie } from 'h3'
2+
import type { ModuleOptions } from '../../../../types'
3+
import { useRuntimeConfig } from '#imports'
4+
import { deleteUser, getCurrentUserFromToken } from '../../../utils/user'
5+
import { hasPermission } from '../../../../utils/permissions'
6+
7+
export default defineEventHandler(async (event) => {
8+
const { nuxtUsers } = useRuntimeConfig()
9+
const options = nuxtUsers as ModuleOptions
10+
const userId = Number(event.context.params?.id)
11+
12+
if (!userId) {
13+
throw createError({
14+
statusCode: 400,
15+
statusMessage: 'Invalid user ID'
16+
})
17+
}
18+
19+
// Check if the user is authenticated
20+
const token = getCookie(event, 'auth_token')
21+
if (!token) {
22+
throw createError({
23+
statusCode: 401,
24+
statusMessage: 'Unauthorized'
25+
})
26+
}
27+
28+
// Get the current user
29+
const currentUser = await getCurrentUserFromToken(token, options)
30+
if (!currentUser) {
31+
throw createError({
32+
statusCode: 401,
33+
statusMessage: 'Unauthorized'
34+
})
35+
}
36+
37+
// Check if the user has permission to delete users (admin role)
38+
if (!hasPermission(currentUser.role, event.path, options.auth.permissions)) {
39+
throw createError({
40+
statusCode: 403,
41+
statusMessage: 'Forbidden'
42+
})
43+
}
44+
45+
try {
46+
// Delete the user
47+
await deleteUser(userId, options)
48+
49+
return { success: true }
50+
} catch (error: any) {
51+
throw createError({
52+
statusCode: 500,
53+
statusMessage: `Error deleting user: ${error.message}`
54+
})
55+
}
56+
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { createError, defineEventHandler, getCookie } from 'h3'
2+
import type { ModuleOptions, UserWithoutPassword } from '../../../../types'
3+
import { useRuntimeConfig } from '#imports'
4+
import { findUserById, getCurrentUserFromToken } from '../../../utils/user'
5+
import { hasPermission } from '../../../../utils/permissions'
6+
7+
export default defineEventHandler(async (event) => {
8+
const { nuxtUsers } = useRuntimeConfig()
9+
const options = nuxtUsers as ModuleOptions
10+
const userId = Number(event.context.params?.id)
11+
12+
if (!userId) {
13+
throw createError({
14+
statusCode: 400,
15+
statusMessage: 'Invalid user ID'
16+
})
17+
}
18+
19+
// Check if the user is authenticated
20+
const token = getCookie(event, 'auth_token')
21+
if (!token) {
22+
throw createError({
23+
statusCode: 401,
24+
statusMessage: 'Unauthorized'
25+
})
26+
}
27+
28+
// Get the current user
29+
const currentUser = await getCurrentUserFromToken(token, options)
30+
if (!currentUser) {
31+
throw createError({
32+
statusCode: 401,
33+
statusMessage: 'Unauthorized'
34+
})
35+
}
36+
37+
// Check if the user is requesting their own data or is an admin
38+
const isAdmin = hasPermission(currentUser.role, event.path, options.auth.permissions)
39+
if (currentUser.id !== userId && !isAdmin) {
40+
throw createError({
41+
statusCode: 403,
42+
statusMessage: 'Forbidden'
43+
})
44+
}
45+
46+
try {
47+
// Fetch the user by ID
48+
const user = await findUserById(userId, options)
49+
50+
if (!user) {
51+
throw createError({
52+
statusCode: 404,
53+
statusMessage: 'User not found'
54+
})
55+
}
56+
57+
return { user }
58+
} catch (error: any) {
59+
throw createError({
60+
statusCode: 500,
61+
statusMessage: `Error fetching user: ${error.message}`
62+
})
63+
}
64+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { createError, defineEventHandler, getCookie, readBody } from 'h3'
2+
import type { ModuleOptions } from '../../../../types'
3+
import { useRuntimeConfig } from '#imports'
4+
import { getCurrentUserFromToken, updateUser } from '../../../utils/user'
5+
import { hasPermission } from '../../../../utils/permissions'
6+
7+
export default defineEventHandler(async (event) => {
8+
const { nuxtUsers } = useRuntimeConfig()
9+
const options = nuxtUsers as ModuleOptions
10+
const userId = Number(event.context.params?.id)
11+
12+
if (!userId) {
13+
throw createError({
14+
statusCode: 400,
15+
statusMessage: 'Invalid user ID'
16+
})
17+
}
18+
19+
// Check if the user is authenticated
20+
const token = getCookie(event, 'auth_token')
21+
if (!token) {
22+
throw createError({
23+
statusCode: 401,
24+
statusMessage: 'Unauthorized'
25+
})
26+
}
27+
28+
// Get the current user
29+
const currentUser = await getCurrentUserFromToken(token, options)
30+
if (!currentUser) {
31+
throw createError({
32+
statusCode: 401,
33+
statusMessage: 'Unauthorized'
34+
})
35+
}
36+
37+
// Check if the user has permission to update users (admin role)
38+
if (!hasPermission(currentUser.role, event.path, options.auth.permissions)) {
39+
throw createError({
40+
statusCode: 403,
41+
statusMessage: 'Forbidden'
42+
})
43+
}
44+
45+
// Get the request body
46+
const body = await readBody(event)
47+
48+
try {
49+
// Update the user
50+
const updatedUser = await updateUser(userId, body, options)
51+
52+
return { user: updatedUser }
53+
} catch (error: any) {
54+
throw createError({
55+
statusCode: 500,
56+
statusMessage: `Error updating user: ${error.message}`
57+
})
58+
}
59+
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { createError, defineEventHandler, getCookie, readBody } from 'h3'
2+
import type { ModuleOptions } from '../../../../types'
3+
import { useRuntimeConfig } from '#imports'
4+
import { createUser, getCurrentUserFromToken } from '../../utils/user'
5+
import { hasPermission } from '../../../utils/permissions'
6+
7+
export default defineEventHandler(async (event) => {
8+
const { nuxtUsers } = useRuntimeConfig()
9+
const options = nuxtUsers as ModuleOptions
10+
11+
// Check if the user is authenticated
12+
const token = getCookie(event, 'auth_token')
13+
if (!token) {
14+
throw createError({
15+
statusCode: 401,
16+
statusMessage: 'Unauthorized'
17+
})
18+
}
19+
20+
// Get the current user
21+
const user = await getCurrentUserFromToken(token, options)
22+
if (!user) {
23+
throw createError({
24+
statusCode: 401,
25+
statusMessage: 'Unauthorized'
26+
})
27+
}
28+
29+
// Check if the user has permission to create users (admin role)
30+
if (!hasPermission(user.role, event.path, options.auth.permissions)) {
31+
throw createError({
32+
statusCode: 403,
33+
statusMessage: 'Forbidden'
34+
})
35+
}
36+
37+
// Get the request body
38+
const body = await readBody(event)
39+
40+
// Validate the request body
41+
if (!body.email || !body.name || !body.password) {
42+
throw createError({
43+
statusCode: 400,
44+
statusMessage: 'Missing required fields: email, name, password'
45+
})
46+
}
47+
48+
try {
49+
// Create the new user
50+
const newUser = await createUser({
51+
email: body.email,
52+
name: body.name,
53+
password: body.password,
54+
role: body.role // role is optional
55+
}, options)
56+
57+
return { user: newUser }
58+
} catch (error: any) {
59+
throw createError({
60+
statusCode: 500,
61+
statusMessage: `Error creating user: ${error.message}`
62+
})
63+
}
64+
})

src/runtime/server/utils/user.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,95 @@ export const findUserByEmail = async (email: string, options: ModuleOptions): Pr
8484
}
8585
}
8686

87+
/**
88+
* Finds a user by their ID.
89+
*/
90+
export const findUserById = async <T extends boolean = false>(
91+
id: number,
92+
options: ModuleOptions,
93+
withPass: T = false as T
94+
): Promise<T extends true ? User | null : UserWithoutPassword | null> => {
95+
const db = await useDb(options)
96+
const usersTable = options.tables.users
97+
98+
const result = await db.sql`SELECT * FROM {${usersTable}} WHERE id = ${id}` as { rows: Array<User> }
99+
100+
if (result.rows.length === 0) {
101+
return null
102+
}
103+
104+
const user = result.rows[0]
105+
106+
const normalizedUser = {
107+
...user,
108+
created_at: user.created_at instanceof Date ? user.created_at.toISOString() : user.created_at,
109+
updated_at: user.updated_at instanceof Date ? user.updated_at.toISOString() : user.updated_at
110+
}
111+
112+
if (withPass === true) {
113+
return normalizedUser as T extends true ? User | null : UserWithoutPassword | null
114+
}
115+
116+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
117+
const { password, ...userWithoutPassword } = normalizedUser
118+
return userWithoutPassword as T extends true ? User | null : UserWithoutPassword | null
119+
}
120+
121+
/**
122+
* Updates a user's details.
123+
*/
124+
export const updateUser = async (id: number, userData: Partial<User>, options: ModuleOptions): Promise<UserWithoutPassword> => {
125+
const db = await useDb(options)
126+
const usersTable = options.tables.users
127+
128+
const currentUser = await findUserById(id, options, true)
129+
if (!currentUser) {
130+
throw new Error('User not found.')
131+
}
132+
133+
const fieldsToUpdate = { ...userData }
134+
// remove id and password from update list
135+
delete fieldsToUpdate.id
136+
delete fieldsToUpdate.password
137+
138+
if (Object.keys(fieldsToUpdate).length === 0) {
139+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
140+
const { password, ...user } = currentUser
141+
return user
142+
}
143+
144+
const entries = Object.entries(fieldsToUpdate)
145+
const setClauses = entries.map(([key], i) => db.sql`${db.escapeIdentifier(key)} = ${entries[i][1]}`)
146+
147+
await db.sql`
148+
UPDATE {${usersTable}}
149+
SET ${db.join(setClauses, ', ')}, updated_at = CURRENT_TIMESTAMP
150+
WHERE id = ${id}
151+
`
152+
153+
const updatedUser = await findUserById(id, options)
154+
if (!updatedUser) {
155+
throw new Error('Failed to retrieve updated user.')
156+
}
157+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
158+
const { password, ...user } = updatedUser
159+
return user
160+
}
161+
162+
/**
163+
* Deletes a user by their ID.
164+
*/
165+
export const deleteUser = async (id: number, options: ModuleOptions): Promise<void> => {
166+
const db = await useDb(options)
167+
const usersTable = options.tables.users
168+
169+
const result = await db.sql`DELETE FROM {${usersTable}} WHERE id = ${id}`
170+
171+
if (result.rows.length === 0) {
172+
throw new Error('User not found or could not be deleted.')
173+
}
174+
}
175+
87176
/**
88177
* Updates a user's password.
89178
* Hashes the new password before storing.

0 commit comments

Comments
 (0)