From e655b835b2a6fac89814828dafefd2b24b4d1f76 Mon Sep 17 00:00:00 2001 From: lolakk05 Date: Fri, 15 May 2026 17:48:05 +0200 Subject: [PATCH 1/6] feat: user management api --- .env.example | 2 +- app/controllers/v1/permissions.ts | 49 +++++++++++++++++++++++++++++++ app/controllers/v1/users.ts | 8 +++++ app/models/user.ts | 30 ++++++++++++++++++- package-lock.json | 10 +++---- 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 app/controllers/v1/users.ts diff --git a/.env.example b/.env.example index fb9ebf1d..788d6cb2 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,4 @@ ACCESS_SECRET=7RK3RKuABKYGUQkeAt8nXTMBk2b5G4BJm4javB1DUiP= REFRESH_PK="ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDAK2dZyfoC7rMBkznEN+oMU9bPUuwfPLZ6EoF53tb5/cWPFnop6TGhyyT0hKsYFMkc=" MINIATURE_MAX_HEIGHT_PX=300 MINIATURE_MAX_PROCESSING_TIME_S=5 -GOOGLE_APPLICATION_CREDENTIALS=./firebase_service_account_key.json +GOOGLE_APPLICATION_CREDENTIALS=./firebase_service_account_key.json \ No newline at end of file diff --git a/app/controllers/v1/permissions.ts b/app/controllers/v1/permissions.ts index 4deb035f..d8028a53 100644 --- a/app/controllers/v1/permissions.ts +++ b/app/controllers/v1/permissions.ts @@ -46,6 +46,13 @@ const permissionChangeValidator = vine.compile( }), ); +const roleChangeValidator = vine.compile( + vine.object({ + userId: vine.number(), + roles: vine.array(vine.string()), + }), +); + const listPermissionsValidator = vine.compile( vine.object({ userId: vine.number(), @@ -57,6 +64,8 @@ export default class PermissionsController extends BaseController { router.post("/allow", [controller, "allow"]).as("allow"); router.post("/revoke", [controller, "revoke"]).as("revoke"); router.get("/list", [controller, "listUserPermissions"]).as("list"); + router.post("roles/assign", [controller, "assignRoles"]).as("assignRoles"); + router.post("roles/revoke", [controller, "revokeRoles"]).as("revokeRoles"); } async allow({ request, auth }: HttpContext) { @@ -162,4 +171,44 @@ export default class PermissionsController extends BaseController { permissions, }; } + + async assignRoles({ request, auth }: HttpContext) { + await this.requireSuperUser(auth); + + const { userId, roles } = await request.validateUsing(roleChangeValidator); + + const targetUser = await User.findOrFail(userId).addErrorContext( + () => `User with id ${userId} not found`, + ); + + const manager = Acl.model(targetUser); + + for (const role of roles) { + await manager + .assignRole(role) + .addErrorContext(`Failed to assign role ${role}`); + } + + return { success: true }; + } + + async revokeRoles({ request, auth }: HttpContext) { + await this.requireSuperUser(auth); + + const { userId, roles } = await request.validateUsing(roleChangeValidator); + + const targetUser = await User.findOrFail(userId).addErrorContext( + () => `User with id ${userId} not found`, + ); + + const manager = Acl.model(targetUser); + + for (const role of roles) { + await manager + .revokeRole(role) + .addErrorContext(`Failed to revoke role ${role}`); + } + + return { success: true }; + } } diff --git a/app/controllers/v1/users.ts b/app/controllers/v1/users.ts new file mode 100644 index 00000000..dd6e6675 --- /dev/null +++ b/app/controllers/v1/users.ts @@ -0,0 +1,8 @@ +import AutoCrudController from "#controllers/auto_crud_controller"; +import User from "#models/user"; + +export default class UsersController extends AutoCrudController { + protected readonly queryRelations = ["userRoles", "userPermissions"]; + protected readonly crudRelations = []; + protected readonly model = User; +} diff --git a/app/models/user.ts b/app/models/user.ts index 01691e8d..513a25d0 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,4 +1,9 @@ -import { MorphMap, hasPermissions } from "@holoyan/adonisjs-permissions"; +import { + MorphMap, + Permission, + Role, + hasPermissions, +} from "@holoyan/adonisjs-permissions"; import { AclModelInterface, ModelIdType, @@ -12,7 +17,12 @@ import { compose } from "@adonisjs/core/helpers"; import hash from "@adonisjs/core/services/hash"; import logger from "@adonisjs/core/services/logger"; import { BaseModel, beforeSave, scope } from "@adonisjs/lucid/orm"; +import { manyToMany } from "@adonisjs/lucid/orm"; +import type { ManyToMany } from "@adonisjs/lucid/types/relations"; +import { preloadRelations } from "#app/scopes/preload_helper"; +import { handleSearchQuery } from "#app/scopes/search_helper"; +import { handleSortQuery } from "#app/scopes/sort_helper"; import { typedColumn } from "#decorators/typed_model"; import { sha256 } from "#utils/hash"; @@ -61,6 +71,24 @@ export default class User static accessTokens = DbAccessTokensProvider.forModel(User); + static preloadRelations = preloadRelations(); + static handleSearchQuery = handleSearchQuery(); + static handleSortQuery = handleSortQuery(); + + @manyToMany(() => Role, { + pivotTable: "model_role", + pivotForeignKey: "model_id", + pivotRelatedForeignKey: "role_id", + }) + declare userRoles: ManyToMany; + + @manyToMany(() => Permission, { + pivotTable: "model_permission", + pivotForeignKey: "model_id", + pivotRelatedForeignKey: "permission_id", + }) + declare userPermissions: ManyToMany; + @beforeSave() static async hashToken(user: User) { if ( diff --git a/package-lock.json b/package-lock.json index 870cab37..a2816a1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2724,7 +2724,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@japa/assert/-/assert-4.2.0.tgz", "integrity": "sha512-Krgrcee01BN1StlVwK5JQP6LL5t3DE3uFNbfFoDTfW7kQuHB0xh6yfaV0hrgcoiEjsqmm2OOsVWeju9aXK4vIA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@poppinss/macroable": "^1.1.0", @@ -4024,7 +4024,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -4052,7 +4052,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -4962,7 +4962,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5392,7 +5392,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" From 28a1d81854a3d38fbffa4e7a18e234c6bd32c80e Mon Sep 17 00:00:00 2001 From: lolakk05 Date: Fri, 15 May 2026 20:44:19 +0200 Subject: [PATCH 2/6] refactor: user contoller to custom one, transactions" --- app/controllers/v1/permissions.ts | 4 +- app/controllers/v1/users.ts | 144 ++++++++++++++++++++++++++++-- app/models/user.ts | 23 +---- 3 files changed, 141 insertions(+), 30 deletions(-) diff --git a/app/controllers/v1/permissions.ts b/app/controllers/v1/permissions.ts index d8028a53..c645eddf 100644 --- a/app/controllers/v1/permissions.ts +++ b/app/controllers/v1/permissions.ts @@ -64,8 +64,8 @@ export default class PermissionsController extends BaseController { router.post("/allow", [controller, "allow"]).as("allow"); router.post("/revoke", [controller, "revoke"]).as("revoke"); router.get("/list", [controller, "listUserPermissions"]).as("list"); - router.post("roles/assign", [controller, "assignRoles"]).as("assignRoles"); - router.post("roles/revoke", [controller, "revokeRoles"]).as("revokeRoles"); + router.post("/roles/assign", [controller, "assignRoles"]).as("assignRoles"); + router.post("/roles/revoke", [controller, "revokeRoles"]).as("revokeRoles"); } async allow({ request, auth }: HttpContext) { diff --git a/app/controllers/v1/users.ts b/app/controllers/v1/users.ts index dd6e6675..5cacef7f 100644 --- a/app/controllers/v1/users.ts +++ b/app/controllers/v1/users.ts @@ -1,8 +1,140 @@ -import AutoCrudController from "#controllers/auto_crud_controller"; -import User from "#models/user"; +import vine from "@vinejs/vine"; -export default class UsersController extends AutoCrudController { - protected readonly queryRelations = ["userRoles", "userPermissions"]; - protected readonly crudRelations = []; - protected readonly model = User; +import { HttpContext } from "@adonisjs/core/http"; +import router from "@adonisjs/core/services/router"; +import { Constructor, LazyImport } from "@adonisjs/core/types/http"; +import db from "@adonisjs/lucid/services/db"; + +import { ForbiddenException } from "#app/exceptions/http_exceptions"; +import User from "#app/models/user"; + +import BaseController from "../base_controller.js"; + +//should check if email is unique and more conditions on password +const createUserValidator = vine.compile( + vine.object({ + fullName: vine.string().optional(), + email: vine.string().email(), + password: vine.string().minLength(8), + }), +); + +const updateUserValidator = vine.compile( + vine.object({ + fullName: vine.string().optional(), + email: vine.string().email().optional(), + password: vine.string().minLength(8).optional(), + }), +); + +export default class UsersController extends BaseController { + $configureRoutes(controller: LazyImport>) { + router.get("/", [controller, "findAll"]).as("users.list"); + router.get("/:id", [controller, "findOne"]).as("users.show"); + router.delete("/:id", [controller, "delete"]).as("users.delete"); + router.patch("/:id", [controller, "update"]).as("users.update"); + router.post("/", [controller, "create"]).as("users.create"); + } + + private async requireSuperUserOrSelf( + auth: HttpContext["auth"], + userId: number, + ): Promise { + if (!auth.isAuthenticated) { + await auth.authenticate(); + } + if (!(await this.isSuperUser(auth)) && auth.user?.id !== userId) { + throw new ForbiddenException(); + } + } + + async findAll({ request, auth }: HttpContext) { + await this.requireSuperUser(auth); + + const page = request.input("page", 1); + const limit = request.input("limit", 10); + + const users = await User.query() + .select("id", "fullName", "email") + .paginate(page, limit); + + return { data: users }; + } + + async findOne({ request, auth }: HttpContext) { + await this.requireSuperUserOrSelf(auth, parseInt(request.param("id"))); + + const userId = request.param("id"); + + const targetUser = await User.query() + .select("id", "fullName", "email") + .where("id", userId) + .firstOrFail() + .addErrorContext(() => `User with id ${userId} not found`); + + return { data: targetUser }; + } + + async delete({ request, auth }: HttpContext) { + await this.requireSuperUserOrSelf(auth, parseInt(request.param("id"))); + + const userId = request.param("id"); + + const targetUser = await User.findOrFail(userId).addErrorContext( + () => `User with id ${userId} not found`, + ); + + await targetUser.delete(); + + return { success: true }; + } + + async update({ request, auth }: HttpContext) { + await this.requireSuperUserOrSelf(auth, parseInt(request.param("id"))); + + const userId = request.param("id"); + + const payload = await request.validateUsing(updateUserValidator); + + const updatedUser = await db.transaction(async (trx) => { + const targetUser = await User.query({ client: trx }) + .where("id", userId) + .firstOrFail() + .addErrorContext(() => `User with id ${userId} not found`); + + targetUser.merge(payload); + await targetUser.save(); + + return targetUser; + }); + + return { + success: true, + data: { + id: updatedUser.id, + fullName: updatedUser.fullName, + email: updatedUser.email, + }, + }; + } + + async create({ request, auth }: HttpContext) { + await this.requireSuperUser(auth); + + const payload = await request.validateUsing(createUserValidator); + + const newUser = await db.transaction(async (trx) => { + const user = await User.create(payload, { client: trx }); + return user; + }); + + return { + success: true, + data: { + id: newUser.id, + fullName: newUser.fullName, + email: newUser.email, + }, + }; + } } diff --git a/app/models/user.ts b/app/models/user.ts index 513a25d0..b821b4e9 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,9 +1,4 @@ -import { - MorphMap, - Permission, - Role, - hasPermissions, -} from "@holoyan/adonisjs-permissions"; +import { MorphMap, hasPermissions } from "@holoyan/adonisjs-permissions"; import { AclModelInterface, ModelIdType, @@ -17,8 +12,6 @@ import { compose } from "@adonisjs/core/helpers"; import hash from "@adonisjs/core/services/hash"; import logger from "@adonisjs/core/services/logger"; import { BaseModel, beforeSave, scope } from "@adonisjs/lucid/orm"; -import { manyToMany } from "@adonisjs/lucid/orm"; -import type { ManyToMany } from "@adonisjs/lucid/types/relations"; import { preloadRelations } from "#app/scopes/preload_helper"; import { handleSearchQuery } from "#app/scopes/search_helper"; @@ -75,20 +68,6 @@ export default class User static handleSearchQuery = handleSearchQuery(); static handleSortQuery = handleSortQuery(); - @manyToMany(() => Role, { - pivotTable: "model_role", - pivotForeignKey: "model_id", - pivotRelatedForeignKey: "role_id", - }) - declare userRoles: ManyToMany; - - @manyToMany(() => Permission, { - pivotTable: "model_permission", - pivotForeignKey: "model_id", - pivotRelatedForeignKey: "permission_id", - }) - declare userPermissions: ManyToMany; - @beforeSave() static async hashToken(user: User) { if ( From e7950744e660e118fb4a8bb9b45b87d9d6cdc4ad Mon Sep 17 00:00:00 2001 From: lolakk05 Date: Fri, 15 May 2026 20:48:37 +0200 Subject: [PATCH 3/6] fix: lint --- app/controllers/v1/users.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/v1/users.ts b/app/controllers/v1/users.ts index 5cacef7f..210807bf 100644 --- a/app/controllers/v1/users.ts +++ b/app/controllers/v1/users.ts @@ -1,8 +1,8 @@ import vine from "@vinejs/vine"; -import { HttpContext } from "@adonisjs/core/http"; +import type { HttpContext } from "@adonisjs/core/http"; import router from "@adonisjs/core/services/router"; -import { Constructor, LazyImport } from "@adonisjs/core/types/http"; +import type { Constructor, LazyImport } from "@adonisjs/core/types/http"; import db from "@adonisjs/lucid/services/db"; import { ForbiddenException } from "#app/exceptions/http_exceptions"; @@ -51,8 +51,8 @@ export default class UsersController extends BaseController { async findAll({ request, auth }: HttpContext) { await this.requireSuperUser(auth); - const page = request.input("page", 1); - const limit = request.input("limit", 10); + const page = request.input("page", 1) as number; + const limit = request.input("limit", 10) as number; const users = await User.query() .select("id", "fullName", "email") From 438603ba91eb3f8560af365e8dd1a697fab83a18 Mon Sep 17 00:00:00 2001 From: lolakk05 Date: Fri, 15 May 2026 20:58:00 +0200 Subject: [PATCH 4/6] fix: fix: lint --- app/controllers/v1/users.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/v1/users.ts b/app/controllers/v1/users.ts index 210807bf..f41668aa 100644 --- a/app/controllers/v1/users.ts +++ b/app/controllers/v1/users.ts @@ -62,9 +62,9 @@ export default class UsersController extends BaseController { } async findOne({ request, auth }: HttpContext) { - await this.requireSuperUserOrSelf(auth, parseInt(request.param("id"))); + const userId = request.param("id") as string; - const userId = request.param("id"); + await this.requireSuperUserOrSelf(auth, Number.parseInt(userId)); const targetUser = await User.query() .select("id", "fullName", "email") @@ -76,9 +76,12 @@ export default class UsersController extends BaseController { } async delete({ request, auth }: HttpContext) { - await this.requireSuperUserOrSelf(auth, parseInt(request.param("id"))); + await this.requireSuperUserOrSelf( + auth, + Number.parseInt(request.param("id") as string), + ); - const userId = request.param("id"); + const userId = request.param("id") as string; const targetUser = await User.findOrFail(userId).addErrorContext( () => `User with id ${userId} not found`, @@ -90,9 +93,9 @@ export default class UsersController extends BaseController { } async update({ request, auth }: HttpContext) { - await this.requireSuperUserOrSelf(auth, parseInt(request.param("id"))); + const userId = request.param("id") as string; - const userId = request.param("id"); + await this.requireSuperUserOrSelf(auth, Number.parseInt(userId)); const payload = await request.validateUsing(updateUserValidator); From 29d83627ed6d6c3820c1b5cec82dbae2268a5787 Mon Sep 17 00:00:00 2001 From: lolakk05 Date: Mon, 18 May 2026 21:21:37 +0200 Subject: [PATCH 5/6] refactor: changes --- app/controllers/v1/users.ts | 57 ++++++++++++++++++------------------- app/models/user.ts | 7 ----- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/app/controllers/v1/users.ts b/app/controllers/v1/users.ts index f41668aa..e1d10a99 100644 --- a/app/controllers/v1/users.ts +++ b/app/controllers/v1/users.ts @@ -5,7 +5,6 @@ import router from "@adonisjs/core/services/router"; import type { Constructor, LazyImport } from "@adonisjs/core/types/http"; import db from "@adonisjs/lucid/services/db"; -import { ForbiddenException } from "#app/exceptions/http_exceptions"; import User from "#app/models/user"; import BaseController from "../base_controller.js"; @@ -27,6 +26,19 @@ const updateUserValidator = vine.compile( }), ); +const paginationValidator = vine.compile( + vine.object({ + page: vine.number().min(1).optional(), + limit: vine.number().min(1).max(100).optional(), + }), +); + +const userIdParamValidator = vine.compile( + vine.object({ + id: vine.number(), + }), +); + export default class UsersController extends BaseController { $configureRoutes(controller: LazyImport>) { router.get("/", [controller, "findAll"]).as("users.list"); @@ -36,23 +48,11 @@ export default class UsersController extends BaseController { router.post("/", [controller, "create"]).as("users.create"); } - private async requireSuperUserOrSelf( - auth: HttpContext["auth"], - userId: number, - ): Promise { - if (!auth.isAuthenticated) { - await auth.authenticate(); - } - if (!(await this.isSuperUser(auth)) && auth.user?.id !== userId) { - throw new ForbiddenException(); - } - } - async findAll({ request, auth }: HttpContext) { await this.requireSuperUser(auth); - const page = request.input("page", 1) as number; - const limit = request.input("limit", 10) as number; + const { page = 1, limit = 10 } = + await request.validateUsing(paginationValidator); const users = await User.query() .select("id", "fullName", "email") @@ -62,29 +62,26 @@ export default class UsersController extends BaseController { } async findOne({ request, auth }: HttpContext) { - const userId = request.param("id") as string; + const user = await request.validateUsing(userIdParamValidator); - await this.requireSuperUserOrSelf(auth, Number.parseInt(userId)); + await this.requireSuperUser(auth); const targetUser = await User.query() .select("id", "fullName", "email") - .where("id", userId) + .where("id", user.id) .firstOrFail() - .addErrorContext(() => `User with id ${userId} not found`); + .addErrorContext(() => `User with id ${user.id} not found`); return { data: targetUser }; } async delete({ request, auth }: HttpContext) { - await this.requireSuperUserOrSelf( - auth, - Number.parseInt(request.param("id") as string), - ); + const user = await request.validateUsing(userIdParamValidator); - const userId = request.param("id") as string; + await this.requireSuperUser(auth); - const targetUser = await User.findOrFail(userId).addErrorContext( - () => `User with id ${userId} not found`, + const targetUser = await User.findOrFail(user.id).addErrorContext( + () => `User with id ${user.id} not found`, ); await targetUser.delete(); @@ -93,17 +90,17 @@ export default class UsersController extends BaseController { } async update({ request, auth }: HttpContext) { - const userId = request.param("id") as string; + const user = await request.validateUsing(userIdParamValidator); - await this.requireSuperUserOrSelf(auth, Number.parseInt(userId)); + await this.requireSuperUser(auth); const payload = await request.validateUsing(updateUserValidator); const updatedUser = await db.transaction(async (trx) => { const targetUser = await User.query({ client: trx }) - .where("id", userId) + .where("id", user.id) .firstOrFail() - .addErrorContext(() => `User with id ${userId} not found`); + .addErrorContext(() => `User with id ${user.id} not found`); targetUser.merge(payload); await targetUser.save(); diff --git a/app/models/user.ts b/app/models/user.ts index b821b4e9..01691e8d 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -13,9 +13,6 @@ import hash from "@adonisjs/core/services/hash"; import logger from "@adonisjs/core/services/logger"; import { BaseModel, beforeSave, scope } from "@adonisjs/lucid/orm"; -import { preloadRelations } from "#app/scopes/preload_helper"; -import { handleSearchQuery } from "#app/scopes/search_helper"; -import { handleSortQuery } from "#app/scopes/sort_helper"; import { typedColumn } from "#decorators/typed_model"; import { sha256 } from "#utils/hash"; @@ -64,10 +61,6 @@ export default class User static accessTokens = DbAccessTokensProvider.forModel(User); - static preloadRelations = preloadRelations(); - static handleSearchQuery = handleSearchQuery(); - static handleSortQuery = handleSortQuery(); - @beforeSave() static async hashToken(user: User) { if ( From 684eeaa6d579c82e169ceca6b03257a2afbd7032 Mon Sep 17 00:00:00 2001 From: lolakk05 Date: Sun, 7 Jun 2026 14:55:51 +0200 Subject: [PATCH 6/6] fix: delete endpoint wrap with transactions and arrow function error creation --- app/controllers/v1/permissions.ts | 8 ++++++-- app/controllers/v1/users.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/controllers/v1/permissions.ts b/app/controllers/v1/permissions.ts index c645eddf..e7a6dcb0 100644 --- a/app/controllers/v1/permissions.ts +++ b/app/controllers/v1/permissions.ts @@ -186,7 +186,9 @@ export default class PermissionsController extends BaseController { for (const role of roles) { await manager .assignRole(role) - .addErrorContext(`Failed to assign role ${role}`); + .addErrorContext( + () => `Failed to assign role ${role} to user ${targetUser.id}`, + ); } return { success: true }; @@ -206,7 +208,9 @@ export default class PermissionsController extends BaseController { for (const role of roles) { await manager .revokeRole(role) - .addErrorContext(`Failed to revoke role ${role}`); + .addErrorContext( + () => `Failed to revoke role ${role} from user ${targetUser.id}`, + ); } return { success: true }; diff --git a/app/controllers/v1/users.ts b/app/controllers/v1/users.ts index e1d10a99..384876af 100644 --- a/app/controllers/v1/users.ts +++ b/app/controllers/v1/users.ts @@ -79,12 +79,13 @@ export default class UsersController extends BaseController { const user = await request.validateUsing(userIdParamValidator); await this.requireSuperUser(auth); + await db.transaction(async (trx) => { + const targetUser = await User.findOrFail(user.id, { + client: trx, + }).addErrorContext(() => `User with id ${user.id} not found`); - const targetUser = await User.findOrFail(user.id).addErrorContext( - () => `User with id ${user.id} not found`, - ); - - await targetUser.delete(); + await targetUser.delete(); + }); return { success: true }; }