feature/#70-static-user-login #97
21 changed files with 395 additions and 79 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import { LoginRoutineEnum } from "../../../enums/loginRoutineEnum";
|
||||||
|
|
||||||
export interface CreateUserCommand {
|
export interface CreateUserCommand {
|
||||||
mail: string;
|
mail: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -5,6 +7,7 @@ export interface CreateUserCommand {
|
||||||
lastname: string;
|
lastname: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
|
routine: LoginRoutineEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateUserCommand {
|
export interface UpdateUserCommand {
|
||||||
|
@ -18,6 +21,7 @@ export interface UpdateUserCommand {
|
||||||
export interface UpdateUserSecretCommand {
|
export interface UpdateUserSecretCommand {
|
||||||
id: string;
|
id: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
routine: LoginRoutineEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransferUserOwnerCommand {
|
export interface TransferUserOwnerCommand {
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default abstract class UserCommandHandler {
|
||||||
lastname: createUser.lastname,
|
lastname: createUser.lastname,
|
||||||
secret: createUser.secret,
|
secret: createUser.secret,
|
||||||
isOwner: createUser.isOwner,
|
isOwner: createUser.isOwner,
|
||||||
|
routine: createUser.routine,
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
@ -75,6 +76,7 @@ export default abstract class UserCommandHandler {
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
secret: updateUser.secret,
|
secret: updateUser.secret,
|
||||||
|
routine: updateUser.routine,
|
||||||
})
|
})
|
||||||
.where("id = :id", { id: updateUser.id })
|
.where("id = :id", { id: updateUser.id })
|
||||||
.execute()
|
.execute()
|
||||||
|
|
|
@ -8,6 +8,25 @@ import UserService from "../service/management/userService";
|
||||||
import speakeasy from "speakeasy";
|
import speakeasy from "speakeasy";
|
||||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||||
import RefreshService from "../service/refreshService";
|
import RefreshService from "../service/refreshService";
|
||||||
|
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Check authentication status by token
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function kickof(req: Request, res: Response): Promise<any> {
|
||||||
|
let username = req.body.username;
|
||||||
|
|
||||||
|
let { routine } = await UserService.getByUsername(username).catch(() => {
|
||||||
|
throw new UnauthorizedRequestException("Username not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
routine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Check authentication status by token
|
* @description Check authentication status by token
|
||||||
|
@ -17,19 +36,27 @@ import RefreshService from "../service/refreshService";
|
||||||
*/
|
*/
|
||||||
export async function login(req: Request, res: Response): Promise<any> {
|
export async function login(req: Request, res: Response): Promise<any> {
|
||||||
let username = req.body.username;
|
let username = req.body.username;
|
||||||
let totp = req.body.totp;
|
let passedSecret = req.body.secret;
|
||||||
|
|
||||||
let { id, secret } = await UserService.getByUsername(username);
|
let { id } = await UserService.getByUsername(username);
|
||||||
|
let { secret, routine } = await UserService.getUserSecretAndRoutine(id);
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
console.log(secret, passedSecret);
|
||||||
secret: secret,
|
|
||||||
encoding: "base32",
|
let valid = false;
|
||||||
token: totp,
|
if (routine == LoginRoutineEnum.totp) {
|
||||||
window: 2,
|
valid = speakeasy.totp.verify({
|
||||||
});
|
secret: secret,
|
||||||
|
encoding: "base32",
|
||||||
|
token: passedSecret,
|
||||||
|
window: 2,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
valid = passedSecret == secret;
|
||||||
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
throw new UnauthorizedRequestException("Credentials not valid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
let accessToken = await JWTHelper.buildToken(id);
|
let accessToken = await JWTHelper.buildToken(id);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import UserService from "../service/management/userService";
|
||||||
import CustomRequestException from "../exceptions/customRequestException";
|
import CustomRequestException from "../exceptions/customRequestException";
|
||||||
import InviteFactory from "../factory/admin/management/invite";
|
import InviteFactory from "../factory/admin/management/invite";
|
||||||
import SettingHelper from "../helpers/settingsHelper";
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get all invites
|
* @description get all invites
|
||||||
|
@ -112,20 +113,26 @@ export async function verifyInvite(req: Request, res: Response): Promise<any> {
|
||||||
*/
|
*/
|
||||||
export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise<any> {
|
export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise<any> {
|
||||||
let mail = req.body.mail;
|
let mail = req.body.mail;
|
||||||
|
let routine = req.body.routine;
|
||||||
let token = req.body.token;
|
let token = req.body.token;
|
||||||
let totp = req.body.totp;
|
let passedSecret = req.body.secret;
|
||||||
|
|
||||||
let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token);
|
let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token);
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
let valid = false;
|
||||||
secret: secret,
|
if (routine == LoginRoutineEnum.totp) {
|
||||||
encoding: "base32",
|
valid = speakeasy.totp.verify({
|
||||||
token: totp,
|
secret: secret,
|
||||||
window: 2,
|
encoding: "base32",
|
||||||
});
|
token: passedSecret,
|
||||||
|
window: 2,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
valid = passedSecret != "";
|
||||||
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
throw new UnauthorizedRequestException("Credentials not valid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
let createUser: CreateUserCommand = {
|
let createUser: CreateUserCommand = {
|
||||||
|
@ -133,8 +140,9 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool
|
||||||
firstname: firstname,
|
firstname: firstname,
|
||||||
lastname: lastname,
|
lastname: lastname,
|
||||||
mail: mail,
|
mail: mail,
|
||||||
secret: secret,
|
secret: routine == LoginRoutineEnum.totp ? secret : passedSecret,
|
||||||
isOwner: grantAdmin,
|
isOwner: grantAdmin,
|
||||||
|
routine,
|
||||||
};
|
};
|
||||||
let id = await UserCommandHandler.create(createUser);
|
let id = await UserCommandHandler.create(createUser);
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import UserService from "../service/management/userService";
|
||||||
import { UpdateUserSecretCommand } from "../command/management/user/userCommand";
|
import { UpdateUserSecretCommand } from "../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||||
import SettingHelper from "../helpers/settingsHelper";
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description request totp reset
|
* @description request totp reset
|
||||||
|
@ -80,27 +81,34 @@ export async function verifyReset(req: Request, res: Response): Promise<any> {
|
||||||
*/
|
*/
|
||||||
export async function finishReset(req: Request, res: Response): Promise<any> {
|
export async function finishReset(req: Request, res: Response): Promise<any> {
|
||||||
let mail = req.body.mail;
|
let mail = req.body.mail;
|
||||||
|
let routine = req.body.routine;
|
||||||
let token = req.body.token;
|
let token = req.body.token;
|
||||||
let totp = req.body.totp;
|
let passedSecret = req.body.secret;
|
||||||
|
|
||||||
let { secret, username } = await ResetService.getByMailAndToken(mail, token);
|
let { secret, username } = await ResetService.getByMailAndToken(mail, token);
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
let valid = false;
|
||||||
secret: secret,
|
if (routine == LoginRoutineEnum.totp) {
|
||||||
encoding: "base32",
|
valid = speakeasy.totp.verify({
|
||||||
token: totp,
|
secret: secret,
|
||||||
window: 2,
|
encoding: "base32",
|
||||||
});
|
token: passedSecret,
|
||||||
|
window: 2,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
valid = passedSecret != "";
|
||||||
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
throw new UnauthorizedRequestException("Credentials not valid or expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
let { id } = await UserService.getByUsername(username);
|
let { id } = await UserService.getByUsername(username);
|
||||||
|
|
||||||
let updateUserSecret: UpdateUserSecretCommand = {
|
let updateUserSecret: UpdateUserSecretCommand = {
|
||||||
id,
|
id,
|
||||||
secret,
|
secret: routine == LoginRoutineEnum.totp ? secret : passedSecret,
|
||||||
|
routine,
|
||||||
};
|
};
|
||||||
await UserCommandHandler.updateSecret(updateUserSecret);
|
await UserCommandHandler.updateSecret(updateUserSecret);
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,15 @@ import QRCode from "qrcode";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import UserFactory from "../factory/admin/management/user";
|
import UserFactory from "../factory/admin/management/user";
|
||||||
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
|
import {
|
||||||
|
TransferUserOwnerCommand,
|
||||||
|
UpdateUserCommand,
|
||||||
|
UpdateUserSecretCommand,
|
||||||
|
} from "../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||||
import SettingHelper from "../helpers/settingsHelper";
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get my by id
|
* @description get my by id
|
||||||
|
@ -22,6 +27,21 @@ export async function getMeById(req: Request, res: Response): Promise<any> {
|
||||||
res.json(UserFactory.mapToSingle(user));
|
res.json(UserFactory.mapToSingle(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get my routine by id
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function getMyRoutine(req: Request, res: Response): Promise<any> {
|
||||||
|
const id = req.userId;
|
||||||
|
let user = await UserService.getById(id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
routine: user.routine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get my totp
|
* @description get my totp
|
||||||
* @param req {Request} Express req object
|
* @param req {Request} Express req object
|
||||||
|
@ -31,7 +51,7 @@ export async function getMeById(req: Request, res: Response): Promise<any> {
|
||||||
export async function getMyTotp(req: Request, res: Response): Promise<any> {
|
export async function getMyTotp(req: Request, res: Response): Promise<any> {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
|
|
||||||
let { secret } = await UserService.getById(userId);
|
let { secret, routine } = await UserService.getUserSecretAndRoutine(userId);
|
||||||
|
|
||||||
const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`;
|
const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`;
|
||||||
|
|
||||||
|
@ -57,7 +77,12 @@ export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
let totp = req.body.totp;
|
let totp = req.body.totp;
|
||||||
|
|
||||||
let { secret } = await UserService.getById(userId);
|
let { secret, routine } = await UserService.getUserSecretAndRoutine(userId);
|
||||||
|
|
||||||
|
if (routine != LoginRoutineEnum.totp) {
|
||||||
|
throw new ForbiddenRequestException("only allowed for totp login");
|
||||||
|
}
|
||||||
|
|
||||||
let valid = speakeasy.totp.verify({
|
let valid = speakeasy.totp.verify({
|
||||||
secret: secret,
|
secret: secret,
|
||||||
encoding: "base32",
|
encoding: "base32",
|
||||||
|
@ -71,6 +96,106 @@ export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description change my password
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function changeMyPassword(req: Request, res: Response): Promise<any> {
|
||||||
|
const userId = req.userId;
|
||||||
|
let current = req.body.current;
|
||||||
|
let newpassword = req.body.newpassword;
|
||||||
|
|
||||||
|
let { secret, routine } = await UserService.getUserSecretAndRoutine(userId);
|
||||||
|
|
||||||
|
if (routine == LoginRoutineEnum.password && current != secret) {
|
||||||
|
throw new ForbiddenRequestException("passwords do not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateUser: UpdateUserSecretCommand = {
|
||||||
|
id: userId,
|
||||||
|
secret: newpassword,
|
||||||
|
routine: LoginRoutineEnum.password,
|
||||||
|
};
|
||||||
|
await UserCommandHandler.updateSecret(updateUser);
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get change to totp
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function getChangeToTOTP(req: Request, res: Response): Promise<any> {
|
||||||
|
var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` });
|
||||||
|
|
||||||
|
QRCode.toDataURL(secret.otpauth_url)
|
||||||
|
.then((result) => {
|
||||||
|
res.json({
|
||||||
|
dataUrl: result,
|
||||||
|
otp: secret.base32,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("QRCode not created", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description change to totp
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function changeToTOTP(req: Request, res: Response): Promise<any> {
|
||||||
|
const userId = req.userId;
|
||||||
|
let otp = req.body.otp;
|
||||||
|
let totp = req.body.totp;
|
||||||
|
|
||||||
|
let valid = speakeasy.totp.verify({
|
||||||
|
secret: otp,
|
||||||
|
encoding: "base32",
|
||||||
|
token: totp,
|
||||||
|
window: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new InternalException("Token not valid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateUser: UpdateUserSecretCommand = {
|
||||||
|
id: userId,
|
||||||
|
secret: otp,
|
||||||
|
routine: LoginRoutineEnum.totp,
|
||||||
|
};
|
||||||
|
await UserCommandHandler.updateSecret(updateUser);
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description change to password
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function changeToPW(req: Request, res: Response): Promise<any> {
|
||||||
|
const userId = req.userId;
|
||||||
|
let newpassword = req.body.newpassword;
|
||||||
|
|
||||||
|
let updateUser: UpdateUserSecretCommand = {
|
||||||
|
id: userId,
|
||||||
|
secret: newpassword,
|
||||||
|
routine: LoginRoutineEnum.password,
|
||||||
|
};
|
||||||
|
await UserCommandHandler.updateSecret(updateUser);
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description transferOwnership
|
* @description transferOwnership
|
||||||
* @param req {Request} Express req object
|
* @param req {Request} Express req object
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { JWTHelper } from "../helpers/jwtHelper";
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
import { JWTToken } from "../type/jwtTypes";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
|
||||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
|
||||||
import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand";
|
|
||||||
import UserService from "../service/management/userService";
|
|
||||||
import speakeasy from "speakeasy";
|
|
||||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
|
||||||
import RefreshService from "../service/refreshService";
|
|
||||||
import WebapiService from "../service/management/webapiService";
|
import WebapiService from "../service/management/webapiService";
|
||||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||||
import WebapiCommandHandler from "../command/management/webapi/webapiCommandHandler";
|
import WebapiCommandHandler from "../command/management/webapi/webapiCommandHandler";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import { DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_TYPE, DB_USERNAME } from "./env.defaults";
|
import { configCheck, DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_TYPE, DB_USERNAME } from "./env.defaults";
|
||||||
|
|
||||||
import { user } from "./entity/management/user";
|
import { user } from "./entity/management/user";
|
||||||
import { refresh } from "./entity/refresh";
|
import { refresh } from "./entity/refresh";
|
||||||
|
@ -55,6 +55,9 @@ import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpd
|
||||||
import { SettingsFromEnv1745059495808 } from "./migrations/1745059495808-settingsFromEnv";
|
import { SettingsFromEnv1745059495808 } from "./migrations/1745059495808-settingsFromEnv";
|
||||||
import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberCreatedAt";
|
import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberCreatedAt";
|
||||||
import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine";
|
import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine";
|
||||||
|
import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set";
|
||||||
|
|
||||||
|
configCheck();
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: DB_TYPE as any,
|
type: DB_TYPE as any,
|
||||||
|
@ -119,6 +122,7 @@ const dataSource = new DataSource({
|
||||||
SettingsFromEnv1745059495808,
|
SettingsFromEnv1745059495808,
|
||||||
SettingsFromEnv_SET1745059495808,
|
SettingsFromEnv_SET1745059495808,
|
||||||
MemberCreatedAt1746006549262,
|
MemberCreatedAt1746006549262,
|
||||||
|
UserLoginRoutine1746252454922,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||||
import { role } from "./role";
|
import { role } from "./role";
|
||||||
import { userPermission } from "./user_permission";
|
import { userPermission } from "./user_permission";
|
||||||
|
import { LoginRoutineEnum } from "../../enums/loginRoutineEnum";
|
||||||
|
import { CodingHelper } from "../../helpers/codingHelper";
|
||||||
|
import { APPLICATION_SECRET } from "../../env.defaults";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class user {
|
export class user {
|
||||||
|
@ -19,11 +22,27 @@ export class user {
|
||||||
@Column({ type: "varchar", length: 255 })
|
@Column({ type: "varchar", length: 255 })
|
||||||
lastname: string;
|
lastname: string;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 255 })
|
@Column({
|
||||||
|
type: "text",
|
||||||
|
select: false,
|
||||||
|
transformer: CodingHelper.entityBaseCoding(APPLICATION_SECRET, "<self>"),
|
||||||
|
})
|
||||||
secret: string;
|
secret: string;
|
||||||
|
|
||||||
@Column({ type: "boolean", default: false })
|
@Column({
|
||||||
static: boolean;
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
default: LoginRoutineEnum.totp,
|
||||||
|
transformer: {
|
||||||
|
to(value: LoginRoutineEnum) {
|
||||||
|
return value.toString();
|
||||||
|
},
|
||||||
|
from(value: string) {
|
||||||
|
return LoginRoutineEnum[value as keyof typeof LoginRoutineEnum];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
routine: LoginRoutineEnum;
|
||||||
|
|
||||||
@Column({ type: "boolean", default: false })
|
@Column({ type: "boolean", default: false })
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
|
|
4
src/enums/loginRoutineEnum.ts
Normal file
4
src/enums/loginRoutineEnum.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum LoginRoutineEnum {
|
||||||
|
password = "password", // login with self defined password
|
||||||
|
totp = "totp", // login with totp by auth apps
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ export const DB_PASSWORD = process.env.DB_PASSWORD ?? "";
|
||||||
|
|
||||||
export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000);
|
export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000);
|
||||||
|
|
||||||
export const APPLICATION_SECRET = process.env.APPLICATION_SECRET;
|
export const APPLICATION_SECRET = process.env.APPLICATION_SECRET ?? "";
|
||||||
|
|
||||||
export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true";
|
export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true";
|
||||||
export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue;
|
export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue;
|
||||||
|
@ -45,6 +45,8 @@ export function configCheck() {
|
||||||
if (DB_USERNAME == "" || typeof DB_USERNAME != "string") throw new Error("set valid value to DB_USERNAME");
|
if (DB_USERNAME == "" || typeof DB_USERNAME != "string") throw new Error("set valid value to DB_USERNAME");
|
||||||
if (DB_PASSWORD == "" || typeof DB_PASSWORD != "string") throw new Error("set valid value to DB_PASSWORD");
|
if (DB_PASSWORD == "" || typeof DB_PASSWORD != "string") throw new Error("set valid value to DB_PASSWORD");
|
||||||
|
|
||||||
|
if (APPLICATION_SECRET == "") throw new Error("set valid APPLICATION_SECRET");
|
||||||
|
|
||||||
if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT");
|
if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT");
|
||||||
|
|
||||||
if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
||||||
|
|
|
@ -2,7 +2,7 @@ import CustomRequestException from "./customRequestException";
|
||||||
|
|
||||||
export default class DatabaseActionException extends CustomRequestException {
|
export default class DatabaseActionException extends CustomRequestException {
|
||||||
constructor(action: string, table: string, err: any) {
|
constructor(action: string, table: string, err: any) {
|
||||||
let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? "XX"}`;
|
let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? err?.message ?? "XX"}`;
|
||||||
super(500, errstring, err);
|
super(500, errstring, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import UserService from "../service/management/userService";
|
||||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||||
import { availableTemplates } from "../type/templateTypes";
|
import { availableTemplates } from "../type/templateTypes";
|
||||||
import SettingHelper from "./settingsHelper";
|
import SettingHelper from "./settingsHelper";
|
||||||
|
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||||
|
|
||||||
export type BackupSection =
|
export type BackupSection =
|
||||||
| "member"
|
| "member"
|
||||||
|
@ -440,6 +441,7 @@ export default abstract class BackupHelper {
|
||||||
"user.firstname",
|
"user.firstname",
|
||||||
"user.lastname",
|
"user.lastname",
|
||||||
"user.secret",
|
"user.secret",
|
||||||
|
"user.routine",
|
||||||
"user.isOwner",
|
"user.isOwner",
|
||||||
])
|
])
|
||||||
.addSelect(["permissions.permission"])
|
.addSelect(["permissions.permission"])
|
||||||
|
@ -806,6 +808,7 @@ export default abstract class BackupHelper {
|
||||||
let roles = await this.transactionManager.getRepository("role").find();
|
let roles = await this.transactionManager.getRepository("role").find();
|
||||||
let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({
|
let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({
|
||||||
...u,
|
...u,
|
||||||
|
routine: u.routine ?? LoginRoutineEnum.totp,
|
||||||
roles: u.roles.map((r: any) => ({
|
roles: u.roles.map((r: any) => ({
|
||||||
...r,
|
...r,
|
||||||
id: roles.find((role) => role.role == r.role)?.id ?? undefined,
|
id: roles.find((role) => role.role == r.role)?.id ?? undefined,
|
||||||
|
|
|
@ -9,12 +9,13 @@ export abstract class CodingHelper {
|
||||||
static entityBaseCoding(key: string = "", fallback: string = ""): ValueTransformer {
|
static entityBaseCoding(key: string = "", fallback: string = ""): ValueTransformer {
|
||||||
return {
|
return {
|
||||||
from(val: string | null | undefined): string {
|
from(val: string | null | undefined): string {
|
||||||
if (!val) return fallback;
|
if (!val || val == "") return fallback;
|
||||||
try {
|
try {
|
||||||
return CodingHelper.decrypt(key, val) || fallback;
|
return CodingHelper.decrypt(key, val, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Decryption error:", error);
|
console.error("Decryption error:", error);
|
||||||
return fallback;
|
if (fallback == "<self>") return val;
|
||||||
|
else return fallback;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
to(val: string | null | undefined): string {
|
to(val: string | null | undefined): string {
|
||||||
|
@ -22,40 +23,47 @@ export abstract class CodingHelper {
|
||||||
if (valueToEncrypt === "") return "";
|
if (valueToEncrypt === "") return "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return CodingHelper.encrypt(key, valueToEncrypt);
|
return CodingHelper.encrypt(key, valueToEncrypt, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Encryption error:", error);
|
console.error("Encryption error:", error);
|
||||||
|
if (fallback == "<self>") return val;
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static encrypt(phrase: string, content: string): string {
|
public static encrypt(phrase: string, content: string, passError = false): string {
|
||||||
if (!content) return "";
|
if (!content) return "";
|
||||||
|
|
||||||
// Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV)
|
try {
|
||||||
const iv = randomBytes(this.ivLength);
|
// Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV)
|
||||||
const key = scryptSync(phrase, "salt", 32);
|
const iv = randomBytes(this.ivLength);
|
||||||
|
const key = scryptSync(phrase, "salt", 32);
|
||||||
|
|
||||||
const cipher = createCipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv));
|
const cipher = createCipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv));
|
||||||
|
|
||||||
// Verschlüssele den Inhalt
|
// Verschlüssele den Inhalt
|
||||||
let encrypted = cipher.update(content, "utf8", "hex");
|
let encrypted = cipher.update(content, "utf8", "hex");
|
||||||
encrypted += cipher.final("hex");
|
encrypted += cipher.final("hex");
|
||||||
|
|
||||||
// Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung)
|
// Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung)
|
||||||
const authTag = cipher.getAuthTag();
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
// Gib das Format: iv:verschlüsselter_text:authTag zurück
|
// Gib das Format: iv:verschlüsselter_text:authTag zurück
|
||||||
return Buffer.concat([
|
return Buffer.concat([
|
||||||
Uint8Array.from(iv),
|
Uint8Array.from(iv),
|
||||||
Uint8Array.from(Buffer.from(encrypted, "hex")),
|
Uint8Array.from(Buffer.from(encrypted, "hex")),
|
||||||
Uint8Array.from(authTag),
|
Uint8Array.from(authTag),
|
||||||
]).toString("base64");
|
]).toString("base64");
|
||||||
|
} catch (error) {
|
||||||
|
if (passError) throw error;
|
||||||
|
console.error("Encryption failed:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static decrypt(phrase: string, content: string): string {
|
public static decrypt(phrase: string, content: string, passError = false): string {
|
||||||
if (!content) return "";
|
if (!content) return "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -79,6 +87,7 @@ export abstract class CodingHelper {
|
||||||
|
|
||||||
return decrypted;
|
return decrypted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (passError) throw error;
|
||||||
console.error("Decryption failed:", error);
|
console.error("Decryption failed:", error);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,12 +56,17 @@ export default abstract class SettingHelper {
|
||||||
return rawValue as unknown as SettingValueMapping[K];
|
return rawValue as unknown as SettingValueMapping[K];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let processedValue = rawValue;
|
||||||
|
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||||
|
processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue);
|
||||||
|
}
|
||||||
|
|
||||||
const baseType =
|
const baseType =
|
||||||
typeof settingType.type === "string"
|
typeof settingType.type === "string"
|
||||||
? (settingType.type.split("/")[0] as SettingTypeAtom)
|
? (settingType.type.split("/")[0] as SettingTypeAtom)
|
||||||
: (settingType.type as SettingTypeAtom);
|
: (settingType.type as SettingTypeAtom);
|
||||||
|
|
||||||
return this.converters[baseType].fromString(rawValue) as unknown as SettingValueMapping[K];
|
return this.converters[baseType].fromString(processedValue) as unknown as SettingValueMapping[K];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,11 +86,11 @@ export default abstract class SettingHelper {
|
||||||
const settingType = settingsType[key];
|
const settingType = settingsType[key];
|
||||||
this.validateSetting(key, stringValue);
|
this.validateSetting(key, stringValue);
|
||||||
|
|
||||||
const oldValue = this.getSetting(key);
|
const oldValue = cloneDeep(this.settings[key]);
|
||||||
let finalValue = stringValue;
|
let newValue = stringValue;
|
||||||
|
|
||||||
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
|
||||||
finalValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue);
|
newValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.settings[key] = stringValue;
|
this.settings[key] = stringValue;
|
||||||
|
@ -94,10 +99,9 @@ export default abstract class SettingHelper {
|
||||||
await SettingCommandHandler.create({
|
await SettingCommandHandler.create({
|
||||||
topic,
|
topic,
|
||||||
key: settingKey,
|
key: settingKey,
|
||||||
value: finalValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newValue = this.getSetting(key);
|
|
||||||
this.notifyListeners(key, newValue, oldValue);
|
this.notifyListeners(key, newValue, oldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
39
src/migrations/1746252454922-UserLoginRoutine.ts
Normal file
39
src/migrations/1746252454922-UserLoginRoutine.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||||
|
import { getDefaultByORM, getTypeByORM } from "./ormHelper";
|
||||||
|
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||||
|
import { CodingHelper } from "../helpers/codingHelper";
|
||||||
|
import { APPLICATION_SECRET } from "../env.defaults";
|
||||||
|
|
||||||
|
export class UserLoginRoutine1746252454922 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let users = await queryRunner.manager.getRepository("user").find({ select: ["id", "secret"] });
|
||||||
|
|
||||||
|
await queryRunner.dropColumns("user", ["secret", "static"]);
|
||||||
|
|
||||||
|
await queryRunner.addColumns("user", [
|
||||||
|
new TableColumn({ name: "secret", ...getTypeByORM("text"), default: getDefaultByORM("string") }),
|
||||||
|
new TableColumn({
|
||||||
|
name: "routine",
|
||||||
|
...getTypeByORM("varchar"),
|
||||||
|
default: getDefaultByORM("string", LoginRoutineEnum.totp),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await queryRunner.manager.getRepository("user").save(users.map((u) => ({ id: u.id, secret: u.secret })));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
let users = await queryRunner.manager.getRepository("user").find({ select: ["id", "secret"] });
|
||||||
|
|
||||||
|
await queryRunner.dropColumn("user", "secret");
|
||||||
|
|
||||||
|
await queryRunner.addColumns("user", [
|
||||||
|
new TableColumn({ name: "secret", ...getTypeByORM("varchar"), default: getDefaultByORM("string") }),
|
||||||
|
new TableColumn({ name: "static", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await queryRunner.manager.getRepository("user").save(users.map((u) => ({ id: u.id, secret: u.secret })));
|
||||||
|
|
||||||
|
await queryRunner.dropColumn("user", "routine");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { login, logout, refresh } from "../controller/authController";
|
import { kickof, login, logout, refresh } from "../controller/authController";
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
router.post("/kickof", async (req, res) => {
|
||||||
|
await kickof(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
router.post("/login", async (req, res) => {
|
||||||
await login(req, res);
|
await login(req, res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,12 @@ router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mai
|
||||||
await verifyInvite(req, res);
|
await verifyInvite(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
router.put(
|
||||||
await finishInvite(req, res);
|
"/",
|
||||||
});
|
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine "]),
|
||||||
|
async (req, res) => {
|
||||||
|
await finishInvite(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -12,8 +12,12 @@ router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username"
|
||||||
await startReset(req, res);
|
await startReset(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
router.put(
|
||||||
await finishReset(req, res);
|
"/",
|
||||||
});
|
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine"]),
|
||||||
|
async (req, res) => {
|
||||||
|
await finishReset(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { getMeById, getMyTotp, transferOwnership, updateMe, verifyMyTotp } from "../controller/userController";
|
import {
|
||||||
|
changeMyPassword,
|
||||||
|
changeToPW,
|
||||||
|
changeToTOTP,
|
||||||
|
getChangeToTOTP,
|
||||||
|
getMeById,
|
||||||
|
getMyRoutine,
|
||||||
|
getMyTotp,
|
||||||
|
transferOwnership,
|
||||||
|
updateMe,
|
||||||
|
verifyMyTotp,
|
||||||
|
} from "../controller/userController";
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
@ -7,14 +18,34 @@ router.get("/me", async (req, res) => {
|
||||||
await getMeById(req, res);
|
await getMeById(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/routine", async (req, res) => {
|
||||||
|
await getMyRoutine(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/totp", async (req, res) => {
|
router.get("/totp", async (req, res) => {
|
||||||
await getMyTotp(req, res);
|
await getMyTotp(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/changeToTOTP", async (req, res) => {
|
||||||
|
await getChangeToTOTP(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/verify", async (req, res) => {
|
router.post("/verify", async (req, res) => {
|
||||||
await verifyMyTotp(req, res);
|
await verifyMyTotp(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/changepw", async (req, res) => {
|
||||||
|
await changeMyPassword(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/changeToTOTP", async (req, res) => {
|
||||||
|
await changeToTOTP(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/changeToPW", async (req, res) => {
|
||||||
|
await changeToPW(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
router.put("/transferOwner", async (req, res) => {
|
router.put("/transferOwner", async (req, res) => {
|
||||||
await transferOwnership(req, res);
|
await transferOwnership(req, res);
|
||||||
});
|
});
|
||||||
|
|
|
@ -129,4 +129,27 @@ export default abstract class UserService {
|
||||||
throw new DatabaseActionException("SELECT", "userRoles", err);
|
throw new DatabaseActionException("SELECT", "userRoles", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get secret and routine by iser
|
||||||
|
* @param userId string
|
||||||
|
* @returns {Promise<user>}
|
||||||
|
*/
|
||||||
|
static async getUserSecretAndRoutine(userId: string): Promise<user> {
|
||||||
|
return await dataSource
|
||||||
|
.getRepository(user)
|
||||||
|
.createQueryBuilder("user")
|
||||||
|
.select("user.id")
|
||||||
|
.addSelect("user.secret")
|
||||||
|
.addSelect("user.routine")
|
||||||
|
.where("user.id = :id", { id: userId })
|
||||||
|
.getOneOrFail()
|
||||||
|
.then((res) => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
throw new DatabaseActionException("SELECT", "user credentials", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue