From 21cadd425f25e8363baac958116769ed31095ff6 Mon Sep 17 00:00:00 2001 From: tmeunier Date: Thu, 19 Mar 2026 17:17:28 +0100 Subject: [PATCH 1/7] feat: add middleware and role enum --- app/enums/role.ts | 6 ++++++ app/middleware/role_middleware.ts | 17 +++++++++++++++++ app/models/user.ts | 4 ++++ ...1773935989197_create_add_role_users_table.ts | 15 +++++++++++++++ start/kernel.ts | 2 ++ 5 files changed, 44 insertions(+) create mode 100644 app/enums/role.ts create mode 100644 app/middleware/role_middleware.ts create mode 100644 database/migrations/1773935989197_create_add_role_users_table.ts diff --git a/app/enums/role.ts b/app/enums/role.ts new file mode 100644 index 0000000..d472395 --- /dev/null +++ b/app/enums/role.ts @@ -0,0 +1,6 @@ +enum Role { + ADMIN = 0, + MAINTAINER = 1, +} + +export default Role diff --git a/app/middleware/role_middleware.ts b/app/middleware/role_middleware.ts new file mode 100644 index 0000000..4030d25 --- /dev/null +++ b/app/middleware/role_middleware.ts @@ -0,0 +1,17 @@ +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' +import Role from '../enums/role.js' + +export default class RoleMiddleware { + async handle({ auth, response }: HttpContext, next: NextFn, options: { roles: Role[] }) { + const user = auth.user! + + if (!options.roles.includes(user.role)) { + return response.forbidden({ + message: 'You do not have permission to access this resource.', + }) + } + + return next() + } +} diff --git a/app/models/user.ts b/app/models/user.ts index dfe4857..e1437c0 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -3,6 +3,7 @@ import hash from '@adonisjs/core/services/hash' import { compose } from '@adonisjs/core/helpers' import { BaseModel, column } from '@adonisjs/lucid/orm' import { withAuthFinder } from '@adonisjs/auth/mixins/lucid' +import Role from '../enums/role.js' const AuthFinder = withAuthFinder(() => hash.use('scrypt'), { uids: ['email'], @@ -22,6 +23,9 @@ export default class User extends compose(BaseModel, AuthFinder) { @column({ serializeAs: null }) declare password: string + @column() + declare role: Role + @column.dateTime({ autoCreate: true }) declare createdAt: DateTime diff --git a/database/migrations/1773935989197_create_add_role_users_table.ts b/database/migrations/1773935989197_create_add_role_users_table.ts new file mode 100644 index 0000000..7d22a76 --- /dev/null +++ b/database/migrations/1773935989197_create_add_role_users_table.ts @@ -0,0 +1,15 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + async up() { + this.schema.table('users', (table) => { + table.integer('role').defaultTo(1) + }) + } + + async down() { + this.schema.table('users', (table) => { + table.dropColumn('role') + }) + } +} diff --git a/start/kernel.ts b/start/kernel.ts index ddd0e49..8751b03 100644 --- a/start/kernel.ts +++ b/start/kernel.ts @@ -39,6 +39,7 @@ router.use([ () => import('@adonisjs/session/session_middleware'), () => import('@adonisjs/shield/shield_middleware'), () => import('@adonisjs/auth/initialize_auth_middleware'), + () => import('#middleware/role_middleware'), ]) /** @@ -48,4 +49,5 @@ router.use([ export const middleware = router.named({ guest: () => import('#middleware/guest_middleware'), auth: () => import('#middleware/auth_middleware'), + role: () => import('#middleware/role_middleware'), }) From 2f7395198da80ba3ffa56b93d31a584905a08fd5 Mon Sep 17 00:00:00 2001 From: tmeunier Date: Fri, 20 Mar 2026 10:05:21 +0100 Subject: [PATCH 2/7] fix: delete role middleware to router.use --- start/kernel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/start/kernel.ts b/start/kernel.ts index 8751b03..6a460cd 100644 --- a/start/kernel.ts +++ b/start/kernel.ts @@ -39,7 +39,6 @@ router.use([ () => import('@adonisjs/session/session_middleware'), () => import('@adonisjs/shield/shield_middleware'), () => import('@adonisjs/auth/initialize_auth_middleware'), - () => import('#middleware/role_middleware'), ]) /** From deaa0d123d69b0d8508a00641adf1b39c29758ff Mon Sep 17 00:00:00 2001 From: tmeunier Date: Mon, 23 Mar 2026 18:25:48 +0100 Subject: [PATCH 3/7] feat: add crud user for administration application --- app/controllers/admin/admin_controller.ts | 7 + .../users/admin_list_users_controller.ts | 14 ++ .../users/admin_remove_users_controller.ts | 18 ++ .../users/admin_store_users_controller.ts | 39 ++++ .../users/admin_update_users_controller.ts | 44 ++++ .../profile/update_profile_controller.ts | 4 +- app/dtos/users/user_dto.ts | 25 +++ app/dtos/users/users_listing_dto.ts | 33 +++ app/models/user.ts | 8 +- config/inertia.ts | 6 +- .../layouts/sidebar/app-sidebar.tsx | 27 ++- inertia/components/ui/select.tsx | 188 ++++++++++++++++++ .../features/admin/layouts/admin-layout.tsx | 60 ++++++ .../components/admin-create-user-modal.tsx | 138 +++++++++++++ .../components/admin-delete-user-dialog.tsx | 72 +++++++ .../components/admin-update-user-dialog.tsx | 136 +++++++++++++ .../profile/components/update-profile.tsx | 4 +- inertia/hooks/use_flash.ts | 2 + inertia/pages/admin/index.tsx | 9 + inertia/pages/admin/users/index.tsx | 70 +++++++ package.json | 1 + start/routes.ts | 29 +++ 22 files changed, 922 insertions(+), 12 deletions(-) create mode 100644 app/controllers/admin/admin_controller.ts create mode 100644 app/controllers/admin/users/admin_list_users_controller.ts create mode 100644 app/controllers/admin/users/admin_remove_users_controller.ts create mode 100644 app/controllers/admin/users/admin_store_users_controller.ts create mode 100644 app/controllers/admin/users/admin_update_users_controller.ts create mode 100644 app/dtos/users/user_dto.ts create mode 100644 app/dtos/users/users_listing_dto.ts create mode 100644 inertia/components/ui/select.tsx create mode 100644 inertia/features/admin/layouts/admin-layout.tsx create mode 100644 inertia/features/admin/users/components/admin-create-user-modal.tsx create mode 100644 inertia/features/admin/users/components/admin-delete-user-dialog.tsx create mode 100644 inertia/features/admin/users/components/admin-update-user-dialog.tsx create mode 100644 inertia/pages/admin/index.tsx create mode 100644 inertia/pages/admin/users/index.tsx diff --git a/app/controllers/admin/admin_controller.ts b/app/controllers/admin/admin_controller.ts new file mode 100644 index 0000000..e2109cb --- /dev/null +++ b/app/controllers/admin/admin_controller.ts @@ -0,0 +1,7 @@ +import { HttpContext } from '@adonisjs/core/http' + +export default class AdminController { + async render({ inertia }: HttpContext) { + return inertia.render('admin/index') + } +} diff --git a/app/controllers/admin/users/admin_list_users_controller.ts b/app/controllers/admin/users/admin_list_users_controller.ts new file mode 100644 index 0000000..7e83862 --- /dev/null +++ b/app/controllers/admin/users/admin_list_users_controller.ts @@ -0,0 +1,14 @@ +// import type { HttpContext } from '@adonisjs/core/http' + +import User from '#models/user' +import { HttpContext } from '@adonisjs/core/http' +import UsersListingDto from '../../../dtos/users/users_listing_dto.js' + +export default class AdminListUserController { + async render({ inertia }: HttpContext) { + const users = await User.all() + const dtos = await Promise.all(users.map((user) => UsersListingDto.fromModel(user))) + + return inertia.render('admin/users/index', { users: dtos }) + } +} diff --git a/app/controllers/admin/users/admin_remove_users_controller.ts b/app/controllers/admin/users/admin_remove_users_controller.ts new file mode 100644 index 0000000..fe0e286 --- /dev/null +++ b/app/controllers/admin/users/admin_remove_users_controller.ts @@ -0,0 +1,18 @@ +import { HttpContext } from '@adonisjs/core/http' +import User from '#models/user' + +export default class AdminRemoveUsersController { + async remove({ request, response, session }: HttpContext) { + const user = await User.findOrFail(request.param('id')) + + if (user.email === 'admin@knowledge.fr') { + session.flash('error', 'You can not remove admin user') + return response.redirect().back() + } + + await user.delete() + + session.flash('success', 'User remove with successfully') + return response.redirect().back() + } +} diff --git a/app/controllers/admin/users/admin_store_users_controller.ts b/app/controllers/admin/users/admin_store_users_controller.ts new file mode 100644 index 0000000..e7d26bc --- /dev/null +++ b/app/controllers/admin/users/admin_store_users_controller.ts @@ -0,0 +1,39 @@ +import { HttpContext } from '@adonisjs/core/http' +import vine from '@vinejs/vine' +import User from '#models/user' +import Role from '../../../enums/role.js' + +export default class AdminStoreUsersController { + static validator = vine.compile( + vine.object({ + username: vine.string().minLength(3).maxLength(255), + email: vine.string().email(), + role: vine.enum(Role), + password: vine.string().minLength(3).maxLength(255), + passwordConfirmation: vine.string().confirmed({ confirmationField: 'password' }), + }) + ) + + async store({ request, response, session }: HttpContext) { + const { username, email, role, password } = await request.validateUsing( + AdminStoreUsersController.validator + ) + + const existingUser = await User.query().where('email', email).first() + + if (existingUser) { + session.flash('error', 'Email already in use !') + return response.redirect().back() + } + + await User.create({ + fullName: username, + role, + email, + password, + }) + + session.flash('success', 'User created successfully !') + return response.redirect().back() + } +} diff --git a/app/controllers/admin/users/admin_update_users_controller.ts b/app/controllers/admin/users/admin_update_users_controller.ts new file mode 100644 index 0000000..b807240 --- /dev/null +++ b/app/controllers/admin/users/admin_update_users_controller.ts @@ -0,0 +1,44 @@ +import vine from '@vinejs/vine' +import Role from '#enums/role' +import { HttpContext } from '@adonisjs/core/http' +import User from '#models/user' + +export default class AdminUpdateUsersController { + static validator = vine.compile( + vine.object({ + username: vine.string().minLength(3).maxLength(255), + email: vine.string().email(), + role: vine.enum(Role), + }) + ) + + async update({ request, response, session }: HttpContext) { + const { username, email, role } = await request.validateUsing( + AdminUpdateUsersController.validator + ) + + if (email === 'admin@knowledge.fr') { + session.flash('error', 'You can not update admin user') + return response.redirect().back() + } + + const existingUser = await User.query() + .where('email', email) + .whereNot('id', request.param('id')) + .first() + + if (existingUser) { + session.flash('error', 'Email already in use !') + return response.redirect().back() + } + + const user = await User.findOrFail(request.param('id')) + user.fullName = username + user.email = email + user.role = role + await user.save() + + session.flash('success', 'User created successfully !') + return response.redirect().back() + } +} diff --git a/app/controllers/profile/update_profile_controller.ts b/app/controllers/profile/update_profile_controller.ts index 1f587d2..6a702a2 100644 --- a/app/controllers/profile/update_profile_controller.ts +++ b/app/controllers/profile/update_profile_controller.ts @@ -1,5 +1,6 @@ import { HttpContext } from '@adonisjs/core/http' import vine from '@vinejs/vine' +import UserDto from '../../dtos/users/user_dto.js' export default class UpdateProfilesController { static validator = vine.compile( @@ -14,8 +15,9 @@ export default class UpdateProfilesController { if (!user) throw new Error('User not found') + const dto = await UserDto.fromModel(user) return inertia.render('profile/index', { - user, + user: dto, }) } diff --git a/app/dtos/users/user_dto.ts b/app/dtos/users/user_dto.ts new file mode 100644 index 0000000..558fdbf --- /dev/null +++ b/app/dtos/users/user_dto.ts @@ -0,0 +1,25 @@ +import Role from '#enums/role' +import User from '#models/user' + +export default class UserDto { + declare id: number + declare email: string + declare username: string + declare role: Role + + constructor(data: { id: number; email: string; username: string; role: Role }) { + this.id = data.id + this.email = data.email + this.username = data.username + this.role = data.role + } + + static async fromModel(user: User): Promise { + return new UserDto({ + id: user.id, + email: user.email, + username: user.fullName, + role: user.role, + }) + } +} diff --git a/app/dtos/users/users_listing_dto.ts b/app/dtos/users/users_listing_dto.ts new file mode 100644 index 0000000..1a34a48 --- /dev/null +++ b/app/dtos/users/users_listing_dto.ts @@ -0,0 +1,33 @@ +import User from '#models/user' + +export default class UsersListingDto { + declare id: number + declare email: string + declare username: string + declare role: string + declare created_at: string + + constructor(data: { + id: number + email: string + username: string + role: string + created_at: string + }) { + this.id = data.id + this.email = data.email + this.username = data.username + this.role = data.role + this.created_at = data.created_at + } + + static async fromModel(user: User): Promise { + return new UsersListingDto({ + id: user.id, + email: user.email, + username: user.fullName, + role: user.roleLabel, + created_at: user.createdAt.toFormat('dd/MM/yyyy'), + }) + } +} diff --git a/app/models/user.ts b/app/models/user.ts index e1437c0..e0185b3 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -3,7 +3,7 @@ import hash from '@adonisjs/core/services/hash' import { compose } from '@adonisjs/core/helpers' import { BaseModel, column } from '@adonisjs/lucid/orm' import { withAuthFinder } from '@adonisjs/auth/mixins/lucid' -import Role from '../enums/role.js' +import Role from '#enums/role' const AuthFinder = withAuthFinder(() => hash.use('scrypt'), { uids: ['email'], @@ -15,7 +15,7 @@ export default class User extends compose(BaseModel, AuthFinder) { declare id: number @column() - declare fullName: string | null + declare fullName: string @column() declare email: string @@ -31,4 +31,8 @@ export default class User extends compose(BaseModel, AuthFinder) { @column.dateTime({ autoCreate: true, autoUpdate: true }) declare updatedAt: DateTime | null + + get roleLabel(): string { + return Role[this.role] + } } diff --git a/config/inertia.ts b/config/inertia.ts index ae8c0d2..4704df7 100644 --- a/config/inertia.ts +++ b/config/inertia.ts @@ -1,6 +1,7 @@ import { defineConfig } from '@adonisjs/inertia' import type { InferSharedProps } from '@adonisjs/inertia/types' import GetTreeByFolderAction from '../app/actions/get_tree_by_folder_action.js' +import UserDto from '../app/dtos/users/user_dto.js' const inertiaConfig = defineConfig({ /** @@ -12,7 +13,10 @@ const inertiaConfig = defineConfig({ * Data that should be shared with all rendered pages */ sharedData: { - user: (ctx) => ctx.inertia.always(() => ctx.auth.user), + user: (ctx) => + ctx.inertia.always(async () => + ctx.auth.user ? await UserDto.fromModel(ctx.auth.user) : null + ), flash: (ctx) => ({ success: ctx.session?.flashMessages.get('success'), error: ctx.session?.flashMessages.get('error'), diff --git a/inertia/components/layouts/sidebar/app-sidebar.tsx b/inertia/components/layouts/sidebar/app-sidebar.tsx index a53cf34..9a24aef 100644 --- a/inertia/components/layouts/sidebar/app-sidebar.tsx +++ b/inertia/components/layouts/sidebar/app-sidebar.tsx @@ -8,7 +8,7 @@ import { SidebarRail, useSidebar, } from '@/components/ui/sidebar' -import { BadgeCheck, ChevronsUpDown, LibraryBig, LogOut, Share2 } from 'lucide-react' +import { BadgeCheck, ChevronsUpDown, LibraryBig, LogOut, Settings, Share2 } from 'lucide-react' import { FragmentLoader } from '@/components/load_fragment' import { DropdownMenu, @@ -22,11 +22,13 @@ import { import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Link, router, usePage } from '@inertiajs/react' import { getInitials } from '@/lib/utils' +import Role from '#enums/role' interface CurrentUserPropsPage { user: { - fullName: string + username: string email: string + role: Role } [key: string]: unknown } @@ -56,11 +58,11 @@ export default function AppSidebar() { > - {getInitials(props.user.fullName)} + {getInitials(props.user.username)}
- {props.user.fullName} + {props.user.username} {props.user.email}
@@ -76,11 +78,11 @@ export default function AppSidebar() {
- {getInitials(props.user.fullName)} + {getInitials(props.user.username)}
- {props.user.fullName} + {props.user.username} {props.user.email}
@@ -100,6 +102,19 @@ export default function AppSidebar() { + {props.user.role === Role.ADMIN && ( + <> + + + + + + Administration + + + + + )} + ) + })} + + + +
{children}
+ + + ) +} diff --git a/inertia/features/admin/users/components/admin-create-user-modal.tsx b/inertia/features/admin/users/components/admin-create-user-modal.tsx new file mode 100644 index 0000000..7c4778b --- /dev/null +++ b/inertia/features/admin/users/components/admin-create-user-modal.tsx @@ -0,0 +1,138 @@ +import { router, useForm } from '@inertiajs/react' +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Field, FieldError, FieldGroup } from '@/components/ui/field' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Loader2 } from 'lucide-react' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +interface AdminCreateUserModalProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export default function AdminCreateUserModal({ open, onOpenChange }: AdminCreateUserModalProps) { + const { data, setData, processing, errors } = useForm({ + username: '', + email: '', + password: '', + passwordConfirmation: '', + role: 0, + }) + + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault() + + router.post('/admin/users/store', data, { + onSuccess: () => { + onOpenChange(false) + }, + }) + } + + return ( + + +
+ + Create user + + +
+ + + setData('username', e.target.value)} + /> + {errors.username && {errors.username}} + + + + setData('email', e.target.value)} + /> + {errors.email && {errors.email}} + +
+ + + + {errors.role && {errors.role}} + +
+ + + setData('password', e.target.value)} + /> + {errors.password && {errors.password}} + + + + setData('passwordConfirmation', e.target.value)} + /> + {errors.passwordConfirmation && ( + {errors.passwordConfirmation} + )} + +
+
+ + + + + + +
+
+
+ ) +} diff --git a/inertia/features/admin/users/components/admin-delete-user-dialog.tsx b/inertia/features/admin/users/components/admin-delete-user-dialog.tsx new file mode 100644 index 0000000..1142f2c --- /dev/null +++ b/inertia/features/admin/users/components/admin-delete-user-dialog.tsx @@ -0,0 +1,72 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Loader2, Trash2 } from 'lucide-react' +import { useForm } from '@inertiajs/react' +import { useState } from 'react' + +interface AdminDeleteUserProps { + id: number +} + +export default function AdminDeleteUserDialog(props: AdminDeleteUserProps) { + const [open, setOpen] = useState(false) + const { delete: destroy, processing } = useForm() + + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault() + + destroy(`/admin/users/${props.id}/remove`, { + onSuccess: () => { + setOpen(false) + }, + }) + } + + return ( + + + + + + +
+ +
+ +
+ + Remove this user ? + + + Are you sure you want to remove this user ? This action cannot be undone. + +
+ + + + + + + + +
+
+
+ ) +} diff --git a/inertia/features/admin/users/components/admin-update-user-dialog.tsx b/inertia/features/admin/users/components/admin-update-user-dialog.tsx new file mode 100644 index 0000000..52a0a14 --- /dev/null +++ b/inertia/features/admin/users/components/admin-update-user-dialog.tsx @@ -0,0 +1,136 @@ +import { router, useForm } from '@inertiajs/react' +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { Field, FieldError, FieldGroup } from '@/components/ui/field' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Loader2, SquarePen } from 'lucide-react' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { useState } from 'react' + +interface AdminUpdateUserModalProps { + user: { + id: number + username: string + email: string + role: string + } +} + +export default function AdminUpdateUserDialog({ user }: AdminUpdateUserModalProps) { + const [open, setOpen] = useState(false) + + const { data, setData, processing, errors } = useForm({ + username: user.username, + email: user.email, + role: user.role === 'ADMIN' ? '0' : '1', + }) + + console.log(user.role, typeof user.role) + + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault() + + router.post( + `/admin/users/${user.id}/update`, + { + ...data, + role: parseInt(data.role), + }, + { + onSuccess: () => { + setOpen(false) + }, + } + ) + } + + return ( + + + + + + +
+ + Update user + + +
+ + + setData('username', e.target.value)} + /> + {errors.username && {errors.username}} + + + + setData('email', e.target.value)} + /> + {errors.email && {errors.email}} + +
+ + + + {errors.role && {errors.role}} + +
+ + + + + + +
+
+
+ ) +} diff --git a/inertia/features/profile/components/update-profile.tsx b/inertia/features/profile/components/update-profile.tsx index e95d187..715ef55 100644 --- a/inertia/features/profile/components/update-profile.tsx +++ b/inertia/features/profile/components/update-profile.tsx @@ -6,14 +6,14 @@ import { Button } from '@/components/ui/button' interface UpdateProfileProps { user: { - fullName: string + username: string email: string } } export default function UpdateProfile({ user }: UpdateProfileProps) { const { data, setData, post, processing, errors } = useForm({ - fullName: user.fullName, + fullName: user.username, email: user.email, }) diff --git a/inertia/hooks/use_flash.ts b/inertia/hooks/use_flash.ts index eb23bbe..4a8c8d8 100644 --- a/inertia/hooks/use_flash.ts +++ b/inertia/hooks/use_flash.ts @@ -4,6 +4,7 @@ import { usePage } from '@inertiajs/react' type FlashMessages = { success?: string + error?: string } export function useFlash() { @@ -11,5 +12,6 @@ export function useFlash() { useEffect(() => { if (flash.success) toast.success(flash.success) + if (flash.error) toast.error(flash.error) }, [flash]) } diff --git a/inertia/pages/admin/index.tsx b/inertia/pages/admin/index.tsx new file mode 100644 index 0000000..a07d000 --- /dev/null +++ b/inertia/pages/admin/index.tsx @@ -0,0 +1,9 @@ +import AdminLayout from '@/features/admin/layouts/admin-layout' + +export default function Admin() { + return ( + +

Hello word

+
+ ) +} diff --git a/inertia/pages/admin/users/index.tsx b/inertia/pages/admin/users/index.tsx new file mode 100644 index 0000000..c0a6874 --- /dev/null +++ b/inertia/pages/admin/users/index.tsx @@ -0,0 +1,70 @@ +import AdminLayout from '@/features/admin/layouts/admin-layout' +import { LucideUsers, Plus } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' +import { useState } from 'react' +import AdminCreateUserModal from '@/features/admin/users/components/admin-create-user-modal' +import AdminDeleteUserDialog from '@/features/admin/users/components/admin-delete-user-dialog' +import AdminUpdateUserDialog from '@/features/admin/users/components/admin-update-user-dialog' + +export default function Users({ users }: { users: any[] }) { + const [openCreateUser, setOpenCreateUser] = useState(false) + + return ( + +
+
+ +

Users

+
+
+ +
+
+ + {users.length === 0 ? ( +
+ +

+
+ ) : ( +
+ + + + Username + Email + Role + Created At + Actions + + + + {users.map((user, index) => ( + + {user.username} + {user.email} + {user.role} + {user.created_at} + {user.email !== 'admin@knowledge.fr' && ( + +
+ + +
+
+ )} +
+ ))} +
+
+
+ )} + + +
+ ) +} diff --git a/package.json b/package.json index b3d20f4..5f1fb66 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "imports": { "#controllers/*": "./app/controllers/*.js", "#exceptions/*": "./app/exceptions/*.js", + "#enums/*": "./app/enums/*.js", "#models/*": "./app/models/*.js", "#mails/*": "./app/mails/*.js", "#services/*": "./app/services/*.js", diff --git a/start/routes.ts b/start/routes.ts index b3fbcab..d2278e5 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -9,6 +9,20 @@ import router from '@adonisjs/core/services/router' import { middleware } from '#start/kernel' +import Role from '#enums/role' + +const AdminUpdateUsersController = () => + import('#controllers/admin/users/admin_update_users_controller') + +const AdminRemoveUsersController = () => + import('#controllers/admin/users/admin_remove_users_controller') + +const AdminController = () => import('#controllers/admin/admin_controller') + +const AdminListUserController = () => import('#controllers/admin/users/admin_list_users_controller') + +const AdminStoreUsersController = () => + import('#controllers/admin/users/admin_store_users_controller') const ShowImagesController = () => import('#controllers/images/show_images_controller') @@ -88,6 +102,21 @@ router //Upload router.post('/upload', [UploadImagesController, 'upload']).as('upload') + + // Admin + router + .group(() => { + router.get('/', [AdminController, 'render']).as('index') + router.get('/users', [AdminListUserController, 'render']).as('users.lists') + router.post('/users/store', [AdminStoreUsersController, 'store']).as('users.store') + router.post('/users/:id/update', [AdminUpdateUsersController, 'update']).as('users.update') + router + .delete('/users/:id/remove', [AdminRemoveUsersController, 'remove']) + .as('users.delete') + }) + .middleware([middleware.role({ roles: [Role.ADMIN] })]) + .prefix('/admin') + .as('admin') }) .middleware([middleware.auth()]) From dcfb2ca92508aa79a0d3e22856651990eeca41f4 Mon Sep 17 00:00:00 2001 From: tmeunier Date: Mon, 23 Mar 2026 18:26:30 +0100 Subject: [PATCH 4/7] fix: add role for admin user in seeder --- database/seeders/user_seeder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/seeders/user_seeder.ts b/database/seeders/user_seeder.ts index 1d63611..052a99c 100644 --- a/database/seeders/user_seeder.ts +++ b/database/seeders/user_seeder.ts @@ -1,5 +1,6 @@ import { BaseSeeder } from '@adonisjs/lucid/seeders' import User from '#models/user' +import Role from '#enums/role' export default class extends BaseSeeder { async run() { @@ -12,6 +13,7 @@ export default class extends BaseSeeder { fullName: 'admin', email: email, password: 'knowledge', + role: Role.ADMIN, }) console.log('Admin user created') From 1379e6af8acac9acb6660fed15d887a6d44e1bfb Mon Sep 17 00:00:00 2001 From: tmeunier Date: Wed, 25 Mar 2026 16:22:23 +0100 Subject: [PATCH 5/7] fea(admin): add shares gestion on administration --- .../shares/admin_list_shares_controller.ts | 15 +++ app/dtos/shares/share_admin_dto.ts | 25 ++++ .../features/admin/layouts/admin-layout.tsx | 94 +++++++++----- inertia/pages/admin/shares/index.tsx | 120 ++++++++++++++++++ start/routes.ts | 4 + 5 files changed, 227 insertions(+), 31 deletions(-) create mode 100644 app/controllers/admin/shares/admin_list_shares_controller.ts create mode 100644 app/dtos/shares/share_admin_dto.ts create mode 100644 inertia/pages/admin/shares/index.tsx diff --git a/app/controllers/admin/shares/admin_list_shares_controller.ts b/app/controllers/admin/shares/admin_list_shares_controller.ts new file mode 100644 index 0000000..fac9677 --- /dev/null +++ b/app/controllers/admin/shares/admin_list_shares_controller.ts @@ -0,0 +1,15 @@ +import { HttpContext } from '@adonisjs/core/http' +import Share from '#models/share' +import ShareListAdminDto from '../../../dtos/shares/share_admin_dto.js' + +export default class AdminListSharesController { + async render({ inertia }: HttpContext) { + const sharesModels = await Share.query().preload('folder') + + const shares = await Promise.all( + sharesModels.map((share) => ShareListAdminDto.fromModel(share)) + ) + + return inertia.render('admin/shares/index', { shares }) + } +} diff --git a/app/dtos/shares/share_admin_dto.ts b/app/dtos/shares/share_admin_dto.ts new file mode 100644 index 0000000..3883d86 --- /dev/null +++ b/app/dtos/shares/share_admin_dto.ts @@ -0,0 +1,25 @@ +import env from '#start/env' +import Share from '#models/share' + +export default class ShareListAdminDto { + declare id: number + declare url: string + declare created_at: string + declare folder_path: string + + constructor(data: { id: number; url: string; folder_path: string; created_at: string }) { + this.id = data.id + this.url = data.url + this.folder_path = data.folder_path + this.created_at = data.created_at + } + + static async fromModel(share: Share): Promise { + return new ShareListAdminDto({ + id: share.id, + url: `${env.get('APP_URL')}/shares/${share.token}/${share.folder.path}`, + folder_path: share.folder.path, + created_at: share.createdAt.toFormat('dd/MM/yyyy'), + }) + } +} diff --git a/inertia/features/admin/layouts/admin-layout.tsx b/inertia/features/admin/layouts/admin-layout.tsx index fa28db6..7980580 100644 --- a/inertia/features/admin/layouts/admin-layout.tsx +++ b/inertia/features/admin/layouts/admin-layout.tsx @@ -1,5 +1,5 @@ import AppLayouts from '@/components/layouts/app-layouts' -import { Share2, Users } from 'lucide-react' +import { FileText, Home, Share2, Users } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { Head, Link, usePage } from '@inertiajs/react' @@ -7,18 +7,40 @@ import { Head, Link, usePage } from '@inertiajs/react' export default function AdminLayout({ children }: { children: React.ReactNode }) { const { url } = usePage() - const navItems = [ + const sections = [ { - id: 'account', - url: '/admin/users', - label: 'Users', - icon: Users, + title: 'Gestion', + items: [ + { + id: 'dashboard', + url: '/admin', + label: 'Dashboard', + icon: Home, + }, + { + id: 'users', + url: '/admin/users', + label: 'Users', + icon: Users, + }, + ], }, { - id: 'shares', - url: '/admins/shares', - label: 'Shares', - icon: Share2, + title: 'Content', + items: [ + { + id: 'files', + url: '/admin/files', + label: 'Files', + icon: FileText, + }, + { + id: 'shares', + url: '/admin/shares', + label: 'Shares', + icon: Share2, + }, + ], }, ] @@ -28,28 +50,38 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
diff --git a/inertia/pages/admin/shares/index.tsx b/inertia/pages/admin/shares/index.tsx new file mode 100644 index 0000000..8db0fc1 --- /dev/null +++ b/inertia/pages/admin/shares/index.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react' +import { toast } from 'sonner' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { Check, Copy, Share2 } from 'lucide-react' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Button } from '@/components/ui/button' +import DeleteShareDialog from '@/features/shares/delete-share-dialog' +import AdminLayout from '@/features/admin/layouts/admin-layout' + +interface ShareProfile { + id: number + url: string + folder_path: string + created_at: string +} +interface AdminSharesProps { + shares: ShareProfile[] +} + +export default function AdminShares({ shares }: AdminSharesProps) { + const [copiedId, setCopiedId] = useState(null) + + const handleCopy = (share: ShareProfile) => { + navigator.clipboard.writeText(share.url).then(() => () => {}) + + setCopiedId(share.id) + toast.success('Link is copied to clipboard!', { + description: share.url, + }) + + setTimeout(() => setCopiedId(null), 2000) + } + + return ( + + +
+
+
+ +

Shares

+
+
+
+ + {shares.length === 0 ? ( +
+ +

+
+ ) : ( +
+ + + + Path + Link + Created At + Actions + + + + {shares.map((share) => ( + + {share.folder_path} + + + + {share.url} + + + + + {share.created_at} + + + +
+ + + + + Copy link + + +
+
+
+ ))} +
+
+
+ )} +
+
+ ) +} diff --git a/start/routes.ts b/start/routes.ts index d2278e5..baabc4b 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -10,6 +10,8 @@ import router from '@adonisjs/core/services/router' import { middleware } from '#start/kernel' import Role from '#enums/role' +const AdminListSharesController = () => + import('#controllers/admin/shares/admin_list_shares_controller') const AdminUpdateUsersController = () => import('#controllers/admin/users/admin_update_users_controller') @@ -113,6 +115,8 @@ router router .delete('/users/:id/remove', [AdminRemoveUsersController, 'remove']) .as('users.delete') + + router.get('/shares', [AdminListSharesController, 'render']).as('shares.lists') }) .middleware([middleware.role({ roles: [Role.ADMIN] })]) .prefix('/admin') From be72ef70b2617afa6f1c01bdaa220f1d6c76b5b4 Mon Sep 17 00:00:00 2001 From: tmeunier Date: Wed, 25 Mar 2026 16:54:24 +0100 Subject: [PATCH 6/7] fea(admin): fix sidebar --- inertia/features/admin/layouts/admin-layout.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/inertia/features/admin/layouts/admin-layout.tsx b/inertia/features/admin/layouts/admin-layout.tsx index 7980580..37830d8 100644 --- a/inertia/features/admin/layouts/admin-layout.tsx +++ b/inertia/features/admin/layouts/admin-layout.tsx @@ -1,5 +1,5 @@ import AppLayouts from '@/components/layouts/app-layouts' -import { FileText, Home, Share2, Users } from 'lucide-react' +import { Home, Share2, Users } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { Head, Link, usePage } from '@inertiajs/react' @@ -28,12 +28,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) { title: 'Content', items: [ - { - id: 'files', - url: '/admin/files', - label: 'Files', - icon: FileText, - }, { id: 'shares', url: '/admin/shares', From 19f4057af672981bfd6aacd7f2c4382fba4d8a8c Mon Sep 17 00:00:00 2001 From: tmeunier Date: Wed, 25 Mar 2026 16:55:01 +0100 Subject: [PATCH 7/7] refactor: normalize code styling for consistency in user admin and select components --- inertia/components/ui/select.tsx | 59 +++++++++++------------------ inertia/pages/admin/users/index.tsx | 9 ++++- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/inertia/components/ui/select.tsx b/inertia/components/ui/select.tsx index 067e9d3..22f4bb1 100644 --- a/inertia/components/ui/select.tsx +++ b/inertia/components/ui/select.tsx @@ -1,34 +1,28 @@ -import * as React from "react" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" -import { Select as SelectPrimitive } from "radix-ui" +import * as React from 'react' +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react' +import { Select as SelectPrimitive } from 'radix-ui' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' -function Select({ - ...props -}: React.ComponentProps) { +function Select({ ...props }: React.ComponentProps) { return } -function SelectGroup({ - ...props -}: React.ComponentProps) { +function SelectGroup({ ...props }: React.ComponentProps) { return } -function SelectValue({ - ...props -}: React.ComponentProps) { +function SelectValue({ ...props }: React.ComponentProps) { return } function SelectTrigger({ className, - size = "default", + size = 'default', children, ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: 'sm' | 'default' }) { return ( ) { return ( @@ -60,9 +54,9 @@ function SelectContent({ {children} @@ -85,14 +79,11 @@ function SelectContent({ ) } -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { +function SelectLabel({ className, ...props }: React.ComponentProps) { return ( ) @@ -132,7 +123,7 @@ function SelectSeparator({ return ( ) @@ -145,10 +136,7 @@ function SelectScrollUpButton({ return ( @@ -163,10 +151,7 @@ function SelectScrollDownButton({ return ( diff --git a/inertia/pages/admin/users/index.tsx b/inertia/pages/admin/users/index.tsx index c0a6874..97564ba 100644 --- a/inertia/pages/admin/users/index.tsx +++ b/inertia/pages/admin/users/index.tsx @@ -1,7 +1,14 @@ import AdminLayout from '@/features/admin/layouts/admin-layout' import { LucideUsers, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' import { useState } from 'react' import AdminCreateUserModal from '@/features/admin/users/components/admin-create-user-modal' import AdminDeleteUserDialog from '@/features/admin/users/components/admin-delete-user-dialog'