Add admin login and password change endpoints

Introduces POST /login and PATCH /password routes for admin authentication and password management. Adds corresponding schema validation for login and password change, enforces stricter password requirements, and updates adminService with JWT-based profile retrieval and improved token handling.
This commit is contained in:
Gilles Lazures 2026-01-18 19:38:24 +01:00
parent d590ecce6d
commit 86349bcf4f
10 changed files with 102 additions and 5 deletions

View File

@ -1,4 +1,21 @@
const express = require("express")
const router = express.Router()
const adminService = require("../../services/adminService")
router.post("/login", async (req, res) => {
const { username, password } = req.body
const result = await adminService.loginAdmin(username, password)
return res.status(200).json(result)
})
router.patch("/password", async (req, res) => {
const token = req.headers.authorization.replace("Bearer ", "")
const profile = await adminService.getAdminProfileByToken(token)
const { newPassword } = req.body
const result = await adminService.changeAdminPassword(profile.id, newPassword)
return res.status(200).json(result)
})
module.exports = router

View File

@ -2,6 +2,10 @@ const z = require("zod")
module.exports = {
GET: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: z.object({
uuid: z.string().uuid()
})

View File

@ -2,6 +2,10 @@ const z = require("zod")
module.exports = {
GET: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: z.object({
uuid: z.string().uuid()
})

View File

@ -6,9 +6,17 @@ const uuidSchema = z.object({
module.exports = {
GET: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: uuidSchema
},
PUT: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
body: z.object({
reasonKey: z.string().min(1),
reasonMessage: z.string().optional(),
@ -21,6 +29,10 @@ module.exports = {
}
},
DELETE: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: uuidSchema
}
}

View File

@ -2,6 +2,10 @@ const z = require("zod")
module.exports = {
DELETE: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: z.object({
hash: z.string().length(64)
})

17
schemas/admin/login.js Normal file
View File

@ -0,0 +1,17 @@
const z = require("zod")
module.exports = {
POST: {
headers: {
"content-type": z.string().regex(/application\/json/i)
},
body: {
username: z.string()
.min(1),
password: z.string()
.min(8, { message: "The password must be at least 8 characters long." })
.regex(/[A-Z]/, { message: "The password must contain a capital letter." })
.regex(/[0-9]/, { message: "The password must contain a number." })
}
}
}

16
schemas/admin/password.js Normal file
View File

@ -0,0 +1,16 @@
const z = require("zod")
module.exports = {
PATCH: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
body: z.object({
newPassword: z.string()
.min(8, { message: "The password must be at least 8 characters long." })
.regex(/[A-Z]/, { message: "The password must contain a capital letter." })
.regex(/[0-9]/, { message: "The password must contain a number." })
})
}
}

View File

@ -2,6 +2,10 @@ const z = require("zod")
module.exports = {
PATCH: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
body: z.object({
newPassword: z.string()
.min(8, { message: "The password must be at least 8 characters long." })

View File

@ -2,12 +2,20 @@ const z = require("zod")
module.exports = {
PUT: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: z.object({
uuid: z.string().uuid(),
hash: z.string().length(64)
})
},
DELETE: {
headers: z.object({
"content-type": z.string().regex(/application\/json/i),
"authorization": z.string().startsWith("Bearer ")
}),
query: z.object({
uuid: z.string().uuid(),
hash: z.string().length(64)

View File

@ -1,6 +1,7 @@
const jwt = require("jsonwebtoken")
const bcrypt = require("bcryptjs")
const userRepository = require("../repositories/userRepository")
const adminRepository = require("../repositories/adminRepository")
const bcrypt = require("bcryptjs")
const { DefaultError } = require("../errors/errors")
const ADMIN_JWT_SECRET = process.env.ADMIN_JWT_SECRET || "udjJLGCOq7m3NmGpdVLJ@#"
@ -28,7 +29,7 @@ async function checkAdminAccess(adminId, requiredPermission) {
}
async function changeAdminPassword(adminId, newPlainPassword) {
if (!newPlainPassword || newPlainPassword.length < 6) {
if (!newPlainPassword || newPlainPassword.length < 8) {
throw new DefaultError(400, "Le mot de passe doit contenir au moins 6 caractères.")
}
@ -52,6 +53,15 @@ async function getAdminProfile(adminId) {
}
}
async function getAdminProfileByToken(accessToken) {
try {
const decoded = jwt.verify(accessToken, { complete: true, json: true })
return getAdminProfile(decoded.sub)
} catch (error) {
throw error
}
}
async function grantPermission(adminId, permissionKey) {
return await adminRepository.assignPermission(adminId, permissionKey)
}
@ -74,7 +84,7 @@ async function loginAdmin(username, password) {
const token = jwt.sign(
{ id: admin.id, username: admin.username, type: "admin" },
ADMIN_JWT_SECRET,
{ expiresIn: "8h" }
{ expiresIn: "8h", subject: admin.id, issuer: "Yggdrasil" }
)
return { token }
@ -147,5 +157,6 @@ module.exports = {
logPlayerAction,
revokePermission,
checkAdminAccess,
changeAdminPassword
changeAdminPassword,
getAdminProfileByToken
}