Merge pull request 'feature/#70-static-user-login' (#97) from feature/#70-static-user-login into develop
Reviewed-on: #97
This commit is contained in:
commit
a64567ce4e
21 changed files with 395 additions and 79 deletions
|
@ -1,3 +1,5 @@
|
|||
import { LoginRoutineEnum } from "../../../enums/loginRoutineEnum";
|
||||
|
||||
export interface CreateUserCommand {
|
||||
mail: string;
|
||||
username: string;
|
||||
|
@ -5,6 +7,7 @@ export interface CreateUserCommand {
|
|||
lastname: string;
|
||||
secret: string;
|
||||
isOwner: boolean;
|
||||
routine: LoginRoutineEnum;
|
||||
}
|
||||
|
||||
export interface UpdateUserCommand {
|
||||
|
@ -18,6 +21,7 @@ export interface UpdateUserCommand {
|
|||
export interface UpdateUserSecretCommand {
|
||||
id: string;
|
||||
secret: string;
|
||||
routine: LoginRoutineEnum;
|
||||
}
|
||||
|
||||
export interface TransferUserOwnerCommand {
|
||||
|
|
|
@ -31,6 +31,7 @@ export default abstract class UserCommandHandler {
|
|||
lastname: createUser.lastname,
|
||||
secret: createUser.secret,
|
||||
isOwner: createUser.isOwner,
|
||||
routine: createUser.routine,
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
|
@ -75,6 +76,7 @@ export default abstract class UserCommandHandler {
|
|||
.update(user)
|
||||
.set({
|
||||
secret: updateUser.secret,
|
||||
routine: updateUser.routine,
|
||||
})
|
||||
.where("id = :id", { id: updateUser.id })
|
||||
.execute()
|
||||
|
|
|
@ -8,6 +8,25 @@ import UserService from "../service/management/userService";
|
|||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
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
|
||||
|
@ -17,19 +36,27 @@ import RefreshService from "../service/refreshService";
|
|||
*/
|
||||
export async function login(req: Request, res: Response): Promise<any> {
|
||||
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({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
console.log(secret, passedSecret);
|
||||
|
||||
let valid = false;
|
||||
if (routine == LoginRoutineEnum.totp) {
|
||||
valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: passedSecret,
|
||||
window: 2,
|
||||
});
|
||||
} else {
|
||||
valid = passedSecret == secret;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
throw new UnauthorizedRequestException("Credentials not valid or expired");
|
||||
}
|
||||
|
||||
let accessToken = await JWTHelper.buildToken(id);
|
||||
|
|
|
@ -16,6 +16,7 @@ import UserService from "../service/management/userService";
|
|||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
import InviteFactory from "../factory/admin/management/invite";
|
||||
import SettingHelper from "../helpers/settingsHelper";
|
||||
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||
|
||||
/**
|
||||
* @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> {
|
||||
let mail = req.body.mail;
|
||||
let routine = req.body.routine;
|
||||
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 valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
let valid = false;
|
||||
if (routine == LoginRoutineEnum.totp) {
|
||||
valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: passedSecret,
|
||||
window: 2,
|
||||
});
|
||||
} else {
|
||||
valid = passedSecret != "";
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
throw new UnauthorizedRequestException("Credentials not valid or expired");
|
||||
}
|
||||
|
||||
let createUser: CreateUserCommand = {
|
||||
|
@ -133,8 +140,9 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool
|
|||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
mail: mail,
|
||||
secret: secret,
|
||||
secret: routine == LoginRoutineEnum.totp ? secret : passedSecret,
|
||||
isOwner: grantAdmin,
|
||||
routine,
|
||||
};
|
||||
let id = await UserCommandHandler.create(createUser);
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import UserService from "../service/management/userService";
|
|||
import { UpdateUserSecretCommand } from "../command/management/user/userCommand";
|
||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||
import SettingHelper from "../helpers/settingsHelper";
|
||||
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||
|
||||
/**
|
||||
* @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> {
|
||||
let mail = req.body.mail;
|
||||
let routine = req.body.routine;
|
||||
let token = req.body.token;
|
||||
let totp = req.body.totp;
|
||||
let passedSecret = req.body.secret;
|
||||
|
||||
let { secret, username } = await ResetService.getByMailAndToken(mail, token);
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
let valid = false;
|
||||
if (routine == LoginRoutineEnum.totp) {
|
||||
valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: passedSecret,
|
||||
window: 2,
|
||||
});
|
||||
} else {
|
||||
valid = passedSecret != "";
|
||||
}
|
||||
|
||||
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 updateUserSecret: UpdateUserSecretCommand = {
|
||||
id,
|
||||
secret,
|
||||
secret: routine == LoginRoutineEnum.totp ? secret : passedSecret,
|
||||
routine,
|
||||
};
|
||||
await UserCommandHandler.updateSecret(updateUserSecret);
|
||||
|
||||
|
|
|
@ -4,10 +4,15 @@ import QRCode from "qrcode";
|
|||
import InternalException from "../exceptions/internalException";
|
||||
import UserService from "../service/management/userService";
|
||||
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 ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||
import SettingHelper from "../helpers/settingsHelper";
|
||||
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||
|
||||
/**
|
||||
* @description get my by id
|
||||
|
@ -22,6 +27,21 @@ export async function getMeById(req: Request, res: Response): Promise<any> {
|
|||
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
|
||||
* @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> {
|
||||
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}`;
|
||||
|
||||
|
@ -57,7 +77,12 @@ export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
|
|||
const userId = req.userId;
|
||||
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({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
|
@ -71,6 +96,106 @@ export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
|
|||
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
|
||||
* @param req {Request} Express req object
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import { Request, Response } from "express";
|
||||
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 ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||
import WebapiCommandHandler from "../command/management/webapi/webapiCommandHandler";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import "reflect-metadata";
|
||||
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 { refresh } from "./entity/refresh";
|
||||
|
@ -55,6 +55,9 @@ import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpd
|
|||
import { SettingsFromEnv1745059495808 } from "./migrations/1745059495808-settingsFromEnv";
|
||||
import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberCreatedAt";
|
||||
import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine";
|
||||
import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set";
|
||||
|
||||
configCheck();
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: DB_TYPE as any,
|
||||
|
@ -119,6 +122,7 @@ const dataSource = new DataSource({
|
|||
SettingsFromEnv1745059495808,
|
||||
SettingsFromEnv_SET1745059495808,
|
||||
MemberCreatedAt1746006549262,
|
||||
UserLoginRoutine1746252454922,
|
||||
],
|
||||
migrationsRun: true,
|
||||
migrationsTransactionMode: "each",
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { role } from "./role";
|
||||
import { userPermission } from "./user_permission";
|
||||
import { LoginRoutineEnum } from "../../enums/loginRoutineEnum";
|
||||
import { CodingHelper } from "../../helpers/codingHelper";
|
||||
import { APPLICATION_SECRET } from "../../env.defaults";
|
||||
|
||||
@Entity()
|
||||
export class user {
|
||||
|
@ -19,11 +22,27 @@ export class user {
|
|||
@Column({ type: "varchar", length: 255 })
|
||||
lastname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
@Column({
|
||||
type: "text",
|
||||
select: false,
|
||||
transformer: CodingHelper.entityBaseCoding(APPLICATION_SECRET, "<self>"),
|
||||
})
|
||||
secret: string;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
static: boolean;
|
||||
@Column({
|
||||
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 })
|
||||
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 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 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_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 (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
||||
|
|
|
@ -2,7 +2,7 @@ import CustomRequestException from "./customRequestException";
|
|||
|
||||
export default class DatabaseActionException extends CustomRequestException {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import UserService from "../service/management/userService";
|
|||
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||
import { availableTemplates } from "../type/templateTypes";
|
||||
import SettingHelper from "./settingsHelper";
|
||||
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
|
||||
|
||||
export type BackupSection =
|
||||
| "member"
|
||||
|
@ -440,6 +441,7 @@ export default abstract class BackupHelper {
|
|||
"user.firstname",
|
||||
"user.lastname",
|
||||
"user.secret",
|
||||
"user.routine",
|
||||
"user.isOwner",
|
||||
])
|
||||
.addSelect(["permissions.permission"])
|
||||
|
@ -806,6 +808,7 @@ export default abstract class BackupHelper {
|
|||
let roles = await this.transactionManager.getRepository("role").find();
|
||||
let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({
|
||||
...u,
|
||||
routine: u.routine ?? LoginRoutineEnum.totp,
|
||||
roles: u.roles.map((r: any) => ({
|
||||
...r,
|
||||
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 {
|
||||
return {
|
||||
from(val: string | null | undefined): string {
|
||||
if (!val) return fallback;
|
||||
if (!val || val == "") return fallback;
|
||||
try {
|
||||
return CodingHelper.decrypt(key, val) || fallback;
|
||||
return CodingHelper.decrypt(key, val, true);
|
||||
} catch (error) {
|
||||
console.error("Decryption error:", error);
|
||||
return fallback;
|
||||
if (fallback == "<self>") return val;
|
||||
else return fallback;
|
||||
}
|
||||
},
|
||||
to(val: string | null | undefined): string {
|
||||
|
@ -22,40 +23,47 @@ export abstract class CodingHelper {
|
|||
if (valueToEncrypt === "") return "";
|
||||
|
||||
try {
|
||||
return CodingHelper.encrypt(key, valueToEncrypt);
|
||||
return CodingHelper.encrypt(key, valueToEncrypt, true);
|
||||
} catch (error) {
|
||||
console.error("Encryption error:", error);
|
||||
if (fallback == "<self>") return val;
|
||||
return "";
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static encrypt(phrase: string, content: string): string {
|
||||
public static encrypt(phrase: string, content: string, passError = false): string {
|
||||
if (!content) return "";
|
||||
|
||||
// Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV)
|
||||
const iv = randomBytes(this.ivLength);
|
||||
const key = scryptSync(phrase, "salt", 32);
|
||||
try {
|
||||
// Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV)
|
||||
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
|
||||
let encrypted = cipher.update(content, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
// Verschlüssele den Inhalt
|
||||
let encrypted = cipher.update(content, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
// Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung)
|
||||
const authTag = cipher.getAuthTag();
|
||||
// Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung)
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Gib das Format: iv:verschlüsselter_text:authTag zurück
|
||||
return Buffer.concat([
|
||||
Uint8Array.from(iv),
|
||||
Uint8Array.from(Buffer.from(encrypted, "hex")),
|
||||
Uint8Array.from(authTag),
|
||||
]).toString("base64");
|
||||
// Gib das Format: iv:verschlüsselter_text:authTag zurück
|
||||
return Buffer.concat([
|
||||
Uint8Array.from(iv),
|
||||
Uint8Array.from(Buffer.from(encrypted, "hex")),
|
||||
Uint8Array.from(authTag),
|
||||
]).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 "";
|
||||
|
||||
try {
|
||||
|
@ -79,6 +87,7 @@ export abstract class CodingHelper {
|
|||
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
if (passError) throw error;
|
||||
console.error("Decryption failed:", error);
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -56,12 +56,17 @@ export default abstract class SettingHelper {
|
|||
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 =
|
||||
typeof settingType.type === "string"
|
||||
? (settingType.type.split("/")[0] 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];
|
||||
this.validateSetting(key, stringValue);
|
||||
|
||||
const oldValue = this.getSetting(key);
|
||||
let finalValue = stringValue;
|
||||
const oldValue = cloneDeep(this.settings[key]);
|
||||
let newValue = stringValue;
|
||||
|
||||
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;
|
||||
|
@ -94,10 +99,9 @@ export default abstract class SettingHelper {
|
|||
await SettingCommandHandler.create({
|
||||
topic,
|
||||
key: settingKey,
|
||||
value: finalValue,
|
||||
value: newValue,
|
||||
});
|
||||
|
||||
const newValue = this.getSetting(key);
|
||||
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 { login, logout, refresh } from "../controller/authController";
|
||||
import { kickof, login, logout, refresh } from "../controller/authController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.post("/kickof", async (req, res) => {
|
||||
await kickof(req, res);
|
||||
});
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
await login(req, res);
|
||||
});
|
||||
|
|
|
@ -8,8 +8,12 @@ router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mai
|
|||
await verifyInvite(req, res);
|
||||
});
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishInvite(req, res);
|
||||
});
|
||||
router.put(
|
||||
"/",
|
||||
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine "]),
|
||||
async (req, res) => {
|
||||
await finishInvite(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -12,8 +12,12 @@ router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username"
|
|||
await startReset(req, res);
|
||||
});
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishReset(req, res);
|
||||
});
|
||||
router.put(
|
||||
"/",
|
||||
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine"]),
|
||||
async (req, res) => {
|
||||
await finishReset(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
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 });
|
||||
|
||||
|
@ -7,14 +18,34 @@ router.get("/me", async (req, res) => {
|
|||
await getMeById(req, res);
|
||||
});
|
||||
|
||||
router.get("/routine", async (req, res) => {
|
||||
await getMyRoutine(req, res);
|
||||
});
|
||||
|
||||
router.get("/totp", async (req, res) => {
|
||||
await getMyTotp(req, res);
|
||||
});
|
||||
|
||||
router.get("/changeToTOTP", async (req, res) => {
|
||||
await getChangeToTOTP(req, res);
|
||||
});
|
||||
|
||||
router.post("/verify", async (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) => {
|
||||
await transferOwnership(req, res);
|
||||
});
|
||||
|
|
|
@ -129,4 +129,27 @@ export default abstract class UserService {
|
|||
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