Compare commits
6 Commits
5130e0a248
...
2a33f45257
Author | SHA1 | Date | |
---|---|---|---|
2a33f45257 | |||
742330d6fe | |||
71c20c8a06 | |||
3b41cf1c5a | |||
b48b34e4e5 | |||
750e36e363 |
@ -4,6 +4,7 @@ import JwtService from "@services/jwt.service";
|
|||||||
import {Logger} from "tslog";
|
import {Logger} from "tslog";
|
||||||
import type {Request, Response} from "express";
|
import type {Request, Response} from "express";
|
||||||
import UserService from "@services/user.service";
|
import UserService from "@services/user.service";
|
||||||
|
import {IReqEditUserData} from "@interfaces/IReqEditUserData";
|
||||||
|
|
||||||
|
|
||||||
const logger = new Logger({ name: "AuthController" });
|
const logger = new Logger({ name: "AuthController" });
|
||||||
@ -233,7 +234,7 @@ async function getUser(req: Request, res: Response) {
|
|||||||
|
|
||||||
//TODO - Implement re-auth by current password in case of password change
|
//TODO - Implement re-auth by current password in case of password change
|
||||||
async function editUser(req: Request, res: Response) {
|
async function editUser(req: Request, res: Response) {
|
||||||
const body = req.body;
|
const body: IReqEditUserData | null = req.body;
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return res
|
return res
|
||||||
.type('application/json')
|
.type('application/json')
|
||||||
@ -278,12 +279,16 @@ async function editUser(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//TODO Interface
|
//TODO Interface
|
||||||
const modifiedData= {}
|
const modifiedData = {
|
||||||
|
}
|
||||||
|
//@ts-ignore
|
||||||
if (body.firstName) modifiedData.firstName = `${body.firstName}`;
|
if (body.firstName) modifiedData.firstName = `${body.firstName}`;
|
||||||
|
//@ts-ignore
|
||||||
if (body.lastName) modifiedData.lastName = `${body.lastName}`;
|
if (body.lastName) modifiedData.lastName = `${body.lastName}`;
|
||||||
|
//@ts-ignore
|
||||||
if (body.displayName) modifiedData.displayName = `${body.displayName}`;
|
if (body.displayName) modifiedData.displayName = `${body.displayName}`;
|
||||||
// Case handled with hashing by the service.
|
//TODO Case handled with hashing by the service.
|
||||||
if (body.password) modifiedData.password = `${body.password}`;
|
//if (body.password) modifiedData.password = `${body.password}`;
|
||||||
|
|
||||||
//Call service
|
//Call service
|
||||||
const EditUserServiceResult = await UserService.edit(`${targetUserId}`, modifiedData);
|
const EditUserServiceResult = await UserService.edit(`${targetUserId}`, modifiedData);
|
||||||
|
6
src/interfaces/IReqEditUserData.ts
Normal file
6
src/interfaces/IReqEditUserData.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface IReqEditUserData {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
displayName?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
@ -1,36 +1,38 @@
|
|||||||
import express, {type Router} from "express";
|
import express, {type Router} from "express";
|
||||||
import UserGuard from "@validators/UserGuard";
|
import UserGuard from "@validators/UserGuard";
|
||||||
import AdminGuard from "@validators/AdminGuard";
|
import AdminGuard from "@validators/AdminGuard";
|
||||||
|
import AuthController from "@controllers/AuthController";
|
||||||
|
|
||||||
|
|
||||||
const router: Router = express.Router();
|
const router: Router = express.Router();
|
||||||
|
|
||||||
router.route('/login').post()
|
router.route('/login').post(AuthController.login)
|
||||||
router.route('/register').post()
|
router.route('/register').post(AuthController.register)
|
||||||
|
|
||||||
// PATCH
|
// PATCH
|
||||||
|
//TODO - To test
|
||||||
router.route('/me')
|
router.route('/me')
|
||||||
.patch(UserGuard)
|
.patch(UserGuard, AuthController.editUser)
|
||||||
|
|
||||||
// GET
|
// GET
|
||||||
router.route('/me')
|
router.route('/me')
|
||||||
.get(UserGuard)
|
.get(UserGuard, AuthController.getSelf)
|
||||||
|
|
||||||
// DELETE
|
// DELETE
|
||||||
router.route('/me')
|
router.route('/me')
|
||||||
.delete(UserGuard)
|
.delete(UserGuard, AuthController.deleteSelf)
|
||||||
|
|
||||||
|
|
||||||
// GET
|
// GET
|
||||||
router.route('/all')
|
router.route('/all')
|
||||||
.get(AdminGuard)
|
.get(AdminGuard, AuthController.getAllUsers)
|
||||||
|
|
||||||
|
|
||||||
// GET
|
// GET
|
||||||
router.route('/user/:targetId')
|
router.route('/user/:targetId')
|
||||||
.get(AdminGuard)
|
.get(AdminGuard, AuthController.getUser)
|
||||||
.patch(AdminGuard)
|
.patch(AdminGuard, AuthController.editUser)
|
||||||
.delete(AdminGuard)
|
.delete(AdminGuard, AuthController.deleteUser)
|
||||||
|
|
||||||
|
|
||||||
export default router
|
export default router
|
@ -8,10 +8,10 @@ const logger = new Logger({ name: "JwtService" });
|
|||||||
*
|
*
|
||||||
* @param {string | Uint8Array} jwt
|
* @param {string | Uint8Array} jwt
|
||||||
* - The JWT token to verify.
|
* - The JWT token to verify.
|
||||||
* @returns {Promise<null | object>}
|
* @returns {Promise<null | JWTPayload>}
|
||||||
* - The payload of the verified JWT token or null if verification fails.
|
* - The payload of the verified JWT token or null if verification fails.
|
||||||
*/
|
*/
|
||||||
async function JwtVerifyService(jwt: string | Uint8Array): Promise<null | object> {
|
async function JwtVerifyService(jwt: string | Uint8Array): Promise<null | JWTPayload> {
|
||||||
try {
|
try {
|
||||||
const result = await Jose.jwtVerify(
|
const result = await Jose.jwtVerify(
|
||||||
jwt,
|
jwt,
|
||||||
|
@ -13,7 +13,7 @@ const access: ConnectionOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class MySqlHandler {
|
class MysqlHandler {
|
||||||
private readonly handlerName: string;
|
private readonly handlerName: string;
|
||||||
private Logger: Logger<unknown>
|
private Logger: Logger<unknown>
|
||||||
private Connection: Connection;
|
private Connection: Connection;
|
||||||
@ -81,9 +81,9 @@ class MySqlHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MySqlService = {
|
const MySqlService = {
|
||||||
Handler : MySqlHandler,
|
Handler : MysqlHandler,
|
||||||
User: {
|
User: {
|
||||||
insert(handler: MySqlHandler, userData: DbUserData) {
|
insert(handler: MysqlHandler, userData: DbUserData) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _now = new Date()
|
const _now = new Date()
|
||||||
const _sql = "INSERT INTO `users`(`username`, `displayName`, `firstName`, `lastName`, `email`, `passwordHash`, `isAdmin`, `isDisabled`, `dob`, `gdpr`, `iat`, `uat`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
const _sql = "INSERT INTO `users`(`username`, `displayName`, `firstName`, `lastName`, `email`, `passwordHash`, `isAdmin`, `isDisabled`, `dob`, `gdpr`, `iat`, `uat`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
@ -109,7 +109,7 @@ const MySqlService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
update(handler: MySqlHandler, userData: DbUserData) {
|
update(handler: MysqlHandler, userData: DbUserData) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -142,7 +142,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getById(handler: MySqlHandler, userId: string): Promise<DbUserData> {
|
getById(handler: MysqlHandler, userId: string): Promise<DbUserData> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "SELECT * FROM `users` WHERE `id` = ?";
|
const _sql = "SELECT * FROM `users` WHERE `id` = ?";
|
||||||
const _values = [userId];
|
const _values = [userId];
|
||||||
@ -154,7 +154,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getAll(handler: MySqlHandler): Promise<Array<DbUserData>> {
|
getAll(handler: MysqlHandler): Promise<Array<DbUserData>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "SELECT * FROM `users`";
|
const _sql = "SELECT * FROM `users`";
|
||||||
try {
|
try {
|
||||||
@ -165,7 +165,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getByUsername(handler: MySqlHandler, username: string) {
|
getByUsername(handler: MysqlHandler, username: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "SELECT * FROM `users` WHERE `username` = ?";
|
const _sql = "SELECT * FROM `users` WHERE `username` = ?";
|
||||||
const _values = [username];
|
const _values = [username];
|
||||||
@ -177,7 +177,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getByEmail(handler: MySqlHandler, email: string) {
|
getByEmail(handler: MysqlHandler, email: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "SELECT * FROM `users` WHERE `email` = ?";
|
const _sql = "SELECT * FROM `users` WHERE `email` = ?";
|
||||||
const _values = [email];
|
const _values = [email];
|
||||||
@ -189,7 +189,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getByDisplayName(handler: MySqlHandler, displayName: string) {
|
getByDisplayName(handler: MysqlHandler, displayName: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "SELECT * FROM `users` WHERE `displayName` = ?";
|
const _sql = "SELECT * FROM `users` WHERE `displayName` = ?";
|
||||||
const _values = [displayName];
|
const _values = [displayName];
|
||||||
@ -201,7 +201,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getAdminStateForId(handler: MySqlHandler, userId: string) : Promise<boolean> {
|
getAdminStateForId(handler: MysqlHandler, userId: string) : Promise<boolean> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "SELECT `isAdmin` FROM `users` WHERE `id` = ?";
|
const _sql = "SELECT `isAdmin` FROM `users` WHERE `id` = ?";
|
||||||
const _values = [userId];
|
const _values = [userId];
|
||||||
@ -217,7 +217,7 @@ const MySqlService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(handler: MySqlHandler, userId: string) {
|
delete(handler: MysqlHandler, userId: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const _sql = "DELETE FROM `users` WHERE `id` = ?";
|
const _sql = "DELETE FROM `users` WHERE `id` = ?";
|
||||||
const _values = [userId];
|
const _values = [userId];
|
||||||
|
234
src/services/user.service.ts
Normal file
234
src/services/user.service.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import {Logger} from "tslog";
|
||||||
|
|
||||||
|
import Argon2id from "@node-rs/argon2";
|
||||||
|
import MySqlService from "@services/mysql.service";
|
||||||
|
|
||||||
|
|
||||||
|
const logger = new Logger({ name: "UserService" });
|
||||||
|
|
||||||
|
const DbHandler = new MySqlService.Handler('UserService')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a user object from the database based on the given username.
|
||||||
|
*
|
||||||
|
* @param {string} username - The username of the user to retrieve.
|
||||||
|
* @returns {Promise<Object | null>} - The user object if found, or null if not found.
|
||||||
|
*/
|
||||||
|
async function getUserFromUsername(username: string): Promise<object | null> {
|
||||||
|
const dbUser = await MySqlService.User.getByUsername(DbHandler, username)
|
||||||
|
if (dbUser === undefined) return null;
|
||||||
|
return dbUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserFromIdService(id: string | undefined) {
|
||||||
|
const dbUser = await MySqlService.User.getById(DbHandler, id);
|
||||||
|
if (dbUser === undefined) return null;
|
||||||
|
return dbUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new user by creating a UserService object, generating a JWT token, and inserting the user into the database.
|
||||||
|
*
|
||||||
|
* @param {Object} sanitizedData - The sanitized user data.
|
||||||
|
* @param {string} sanitizedData.username - The username of the new user.
|
||||||
|
* @param {string} sanitizedData.displayName - The display namcoe of the new user.
|
||||||
|
* @param {string} sanitizedData.firstName
|
||||||
|
* @param {string} sanitizedData.lastName
|
||||||
|
* @param {string} sanitizedData.password - The password of the new user.
|
||||||
|
* @param {boolean} sanitizedData.gdpr - Indicates whether the new user has accepted GDPR.
|
||||||
|
*
|
||||||
|
* @returns {Object} - An object containing the registered user's data and JWT token.
|
||||||
|
* @returns {string} error - The error name, if any. "none" if registration was successful.
|
||||||
|
* @returns {string|null} jwt - The JWT token for the registered user. Null if registration was not successful.
|
||||||
|
* @returns {Object|null} user - The registered user's data. Null if registration was not successful.
|
||||||
|
* @returns {string|null} user.id - The ID of the registered user. Null if registration was not successful.
|
||||||
|
* @returns {string|null} user.username - The username of the registered user. Null if registration was not successful.
|
||||||
|
* @returns {string|null} user.displayName - The display name of the registered user. Null if registration was not successful.
|
||||||
|
*/
|
||||||
|
async function RegisterService(sanitizedData) {
|
||||||
|
if (sanitizedData.password.length < 6) {
|
||||||
|
logger.info(`REGISTER :> Invalid password (${sanitizedData.username})`)
|
||||||
|
return { error: "invalidPassword" };
|
||||||
|
}
|
||||||
|
const passwordHash = await getHashFromPassword(sanitizedData.password)
|
||||||
|
|
||||||
|
// Does the new user has accepted GDPR ?
|
||||||
|
if (sanitizedData.gdpr !== true) {
|
||||||
|
logger.info(`REGISTER :> GDPR not validated (${sanitizedData.username})`)
|
||||||
|
return { error: "gdprNotApproved" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if exist and return
|
||||||
|
|
||||||
|
const dbUserIfExist = await getUserFromUsername(sanitizedData.username)
|
||||||
|
if (dbUserIfExist) {
|
||||||
|
logger.info(`REGISTER :> User exist (${dbUserIfExist.username})\n ID:${dbUserIfExist.id}`)
|
||||||
|
return { error: "exist" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
|
||||||
|
// New UserService (class)
|
||||||
|
|
||||||
|
const NewUser = new User(sanitizedData.username, sanitizedData.displayName, passwordHash, currentDate);
|
||||||
|
NewUser.setFirstName(sanitizedData.firstName);
|
||||||
|
NewUser.setLastName(sanitizedData.lastName);
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
|
||||||
|
const alg = 'HS512'
|
||||||
|
const token = await JwtSign({
|
||||||
|
sub: NewUser.id
|
||||||
|
}, alg,
|
||||||
|
'1d',
|
||||||
|
'user')
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
error: "none",
|
||||||
|
jwt: token,
|
||||||
|
user: {
|
||||||
|
id: NewUser.id,
|
||||||
|
username: NewUser.username,
|
||||||
|
displayName: NewUser.displayName,
|
||||||
|
firstName: NewUser.firstName,
|
||||||
|
lastName: NewUser.lastName
|
||||||
|
}};
|
||||||
|
logger.info(userData)
|
||||||
|
await Db.collection("users").insertOne(NewUser);
|
||||||
|
logger.info(`REGISTER :> Inserted new user (${NewUser.username})`)
|
||||||
|
return userData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the login process by verifying the provided credentials.
|
||||||
|
* @param {Object} sanitizedData - The sanitized user login data.
|
||||||
|
* @param {string} sanitizedData.username - The username provided by the user.
|
||||||
|
* @param {string} sanitizedData.password - The password provided by the user.
|
||||||
|
* @returns {Object} - The login result object.
|
||||||
|
* @returns {string} result.error - The error code if there is an error during the login process.
|
||||||
|
* @returns {string} result.jwt - The JSON Web Token (JWT) generated upon successful login.
|
||||||
|
* @returns {Object} result.user - The user information.
|
||||||
|
* @returns {number} result.user.id - The ID of the user.
|
||||||
|
* @returns {string} result.user.username - The username of the user.
|
||||||
|
* @returns {string} result.user.displayName - The display name of the user.
|
||||||
|
*/
|
||||||
|
async function LoginService(sanitizedData) {
|
||||||
|
//const passwordHash = await getHashFromPassword(sanitizedData.password);
|
||||||
|
const dbUser = await getUserFromUsername(sanitizedData.username);
|
||||||
|
if (!dbUser) {
|
||||||
|
console.log(`LoginService :> User does not exist (${sanitizedData.username})`);
|
||||||
|
return { error: "userNotFound" };
|
||||||
|
}
|
||||||
|
if (sanitizedData.password.length < 6) {
|
||||||
|
console.log('X')
|
||||||
|
console.log(`LoginService :> Invalid password (${sanitizedData.username})`);
|
||||||
|
return { error: "invalidPassword" };
|
||||||
|
}
|
||||||
|
const isPasswordValid = await Argon2id.verify(
|
||||||
|
Buffer.from(dbUser.passwordHash),
|
||||||
|
Buffer.from(sanitizedData.password),
|
||||||
|
{
|
||||||
|
secret: Buffer.from(`${process.env.HASH_SECRET}`),
|
||||||
|
algorithm: 2
|
||||||
|
});
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
console.log(isPasswordValid)
|
||||||
|
console.log(`LoginService :> Invalid password (${sanitizedData.username})`);
|
||||||
|
return { error: "invalidPassword" };
|
||||||
|
}
|
||||||
|
// biome-ignore lint/style/useConst: <explanation>
|
||||||
|
let userData = {
|
||||||
|
error: "none",
|
||||||
|
jwt: null,
|
||||||
|
user: {
|
||||||
|
id: dbUser.id,
|
||||||
|
username: dbUser.username,
|
||||||
|
displayName: dbUser.displayName,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const alg = 'HS512';
|
||||||
|
userData.jwt = await JwtSign({sub: dbUser.id}, alg, '1d', 'user')
|
||||||
|
|
||||||
|
|
||||||
|
console.log("USERDATA :>");
|
||||||
|
console.log(userData);
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all users from the database.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @function getAllUsersService
|
||||||
|
* @returns {Promise<{iat: number, users: Array<user>, length: number}>} - The response object containing the users array and its length.
|
||||||
|
*/
|
||||||
|
async function getAllUsersService() {
|
||||||
|
const users = await Db.collection("users").find().toArray();
|
||||||
|
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||||
|
users.forEach(user => {
|
||||||
|
delete user.passwordHash
|
||||||
|
delete user._id
|
||||||
|
delete user.gdpr
|
||||||
|
});
|
||||||
|
logger.info(`Query ${users.length} user(s)`)
|
||||||
|
return {
|
||||||
|
iat: Date.now(),
|
||||||
|
users: users,
|
||||||
|
length: users.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits a user in the database.
|
||||||
|
*
|
||||||
|
* @param {string} targetId - The ID of the user to be edited.
|
||||||
|
* @param {object} sanitizedData - The sanitized data to update the user with.
|
||||||
|
* @returns {object} - An object indicating the result of the operation.
|
||||||
|
* If the user is not found, the error property will be a string "userNotFound".
|
||||||
|
* Otherwise, the error property will be a string "none".
|
||||||
|
*/
|
||||||
|
async function editUserService(targetId, sanitizedData) {
|
||||||
|
if (sanitizedData.password) {
|
||||||
|
const passwordHash = await getHashFromPassword(sanitizedData.password)
|
||||||
|
delete sanitizedData.password
|
||||||
|
logger.info(`Changing password for user "${targetId}"`)
|
||||||
|
sanitizedData.passwordHash = passwordHash
|
||||||
|
}
|
||||||
|
const updatedUserResult = await Db.collection("users").updateOne({id: targetId}, {$set: sanitizedData});
|
||||||
|
if (updatedUserResult.modifiedCount === 0) {
|
||||||
|
logger.info(`EDIT :> User not found (${targetId})`);
|
||||||
|
return { error: "userNotFound" };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`EDIT :> User updated (${targetId})`);
|
||||||
|
return { error: "none" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user from the database.
|
||||||
|
*
|
||||||
|
* @param {string} targetId - The ID of the user to be deleted.
|
||||||
|
* @return {Promise<boolean>} - A promise that resolves to true if the user is successfully deleted, or false if an error occurs.
|
||||||
|
*/
|
||||||
|
async function deleteUserService(targetId) {
|
||||||
|
logger.info(`Deleting user ${targetId}`)
|
||||||
|
try {
|
||||||
|
await Db.collection("users").deleteOne({id: targetId});
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserService = {
|
||||||
|
register: RegisterService,
|
||||||
|
login: LoginService,
|
||||||
|
getAll: getAllUsersService,
|
||||||
|
getFromId: getUserFromIdService,
|
||||||
|
edit: editUserService,
|
||||||
|
delete: deleteUserService
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserService;
|
Loading…
x
Reference in New Issue
Block a user