Start implementing a new user page for account and security settings

This commit is contained in:
Fabian Stamm
2023-04-09 18:20:43 +02:00
parent 1e2bb83447
commit 922ed1e813
46 changed files with 2307 additions and 443 deletions

View File

@ -45,6 +45,7 @@
"@hibas123/config": "^1.1.2",
"@hibas123/nodelogging": "^3.1.3",
"@hibas123/nodeloggingserver_client": "^1.1.2",
"@hibas123/openauth-internalapi": "workspace:^",
"@hibas123/openauth-views-v1": "workspace:^",
"@hibas123/safe_mongo": "^1.7.1",
"body-parser": "^1.20.2",

View File

@ -7,6 +7,7 @@ import ClientRouter from "./client";
import * as cors from "cors";
import OAuthRoute from "./oauth";
import config from "../config";
import JRPCEndpoint from "./jrpc";
const ApiRouter: express.IRouter = express.Router();
ApiRouter.use("/admin", AdminRoute);
@ -17,6 +18,26 @@ ApiRouter.use("/oauth", OAuthRoute);
ApiRouter.use("/client", ClientRouter);
/**
* @api {post} /jrpc
* @apiName InternalJRPCEndpoint
*
* @apiGroup user
* @apiPermission none
*
* @apiErrorExample {Object} Error-Response:
{
error: [
{
message: "Some Error",
field: "username"
}
],
status: 400
}
*/
ApiRouter.post("/jrpc", JRPCEndpoint);
// Legacy reasons (deprecated)
ApiRouter.use("/", ClientRouter);

View File

@ -0,0 +1,49 @@
import { Account, ContactInfo, Gender, Server, UserRegisterInfo } from "@hibas123/openauth-internalapi";
import type { SessionContext } from "./index";
import Mail from "../../models/mail";
import User from "../../models/user";
export default class AccountService extends Server.AccountService<SessionContext> {
Register(regcode: string, info: UserRegisterInfo, ctx: SessionContext): Promise<void> {
throw new Error("Method not implemented.");
}
async GetProfile(ctx: SessionContext): Promise<Account> {
if (!ctx.user) throw new Error("Not logged in");
return {
id: ctx.user.uid,
username: ctx.user.username,
name: ctx.user.name,
birthday: ctx.user.birthday.valueOf(),
gender: ctx.user.gender as number as Gender,
}
}
async UpdateProfile(info: Account, ctx: SessionContext): Promise<void> {
if (!ctx.user) throw new Error("Not logged in");
ctx.user.name = info.name;
ctx.user.birthday = new Date(info.birthday);
ctx.user.gender = info.gender as number;
await User.save(ctx.user);
}
async GetContactInfos(ctx: SessionContext): Promise<ContactInfo> {
if (!ctx.user) throw new Error("Not logged in");
let mails = await Promise.all(
ctx.user.mails.map((mail) => Mail.findById(mail))
);
let contact = {
mail: mails.filter((e) => !!e),
phone: ctx.user.phones,
};
return contact;
}
}

View File

@ -0,0 +1,45 @@
import { Request, Response } from "express";
import Stacker from "../middlewares/stacker";
import { GetUserMiddleware } from "../middlewares/user";
import { IUser } from "../../models/user";
import { Server } from "@hibas123/openauth-internalapi";
import AccountService from "./account_service";
import SecurityService from "./security_service";
import { ILoginToken } from "../../models/login_token";
export interface SessionContext {
user: IUser,
request: Request,
isAdmin: boolean,
special: boolean,
token: {
login: ILoginToken,
special?: ILoginToken,
}
}
const provider = new Server.ServiceProvider<SessionContext>();
provider.addService(new AccountService());
provider.addService(new SecurityService());
const JRPCEndpoint = Stacker(
GetUserMiddleware(true, true),
async (req: Request, res: Response) => {
const session = provider.getSession((data) => {
res.json(data);
}, {
user: req.user,
request: req,
isAdmin: req.isAdmin,
special: req.special,
token: {
login: req.token.login,
special: req.token.special,
}
});
session.onMessage(req.body);
}
);
export default JRPCEndpoint;

View File

@ -0,0 +1,71 @@
import { Server, Token, TwoFactor, UserRegisterInfo } from "@hibas123/openauth-internalapi";
import type { SessionContext } from "./index";
import LoginToken, { CheckToken } from "../../models/login_token";
import TwoFactorModel from "../../models/twofactor";
import moment = require("moment");
export default class SecurityService extends Server.SecurityService<SessionContext> {
async GetTokens(ctx: SessionContext): Promise<Token[]> {
if (!ctx.user) throw new Error("Not logged in");
let raw_token = await LoginToken.find({
user: ctx.user._id,
valid: true,
});
let token = await Promise.all(
raw_token
.map<Promise<Token>>(async (token) => {
await CheckToken(token);
return {
id: token._id.toString(),
special: token.special,
ip: token.ip,
browser: token.browser,
isthis: token._id.equals(
token.special ? ctx.token.special._id : ctx.token.login._id
),
};
})
.filter((t) => t !== undefined)
);
return token
}
async RevokeToken(id: string, ctx: SessionContext): Promise<void> {
if (!ctx.user) throw new Error("Not logged in");
let token = await LoginToken.findById(id);
if (!token || !token.user.equals(ctx.user._id))
throw new Error("Invalid ID");
token.valid = false;
await LoginToken.save(token);
}
async GetTwofactorOptions(ctx: SessionContext): Promise<TwoFactor[]> {
if (!ctx.user) throw new Error("Not logged in");
let twofactor = await TwoFactorModel.find({ user: ctx.user._id, valid: true });
let expired = twofactor.filter((e) =>
e.expires ? moment().isAfter(moment(e.expires)) : false
);
await Promise.all(
expired.map((e) => {
e.valid = false;
return TwoFactorModel.save(e);
})
);
twofactor = twofactor.filter((e) => e.valid);
let tfa = twofactor.map<TwoFactor>((e) => {
return {
id: e._id.toString(),
name: e.name,
tfatype: e.type as number,
expires: e.expires?.valueOf()
};
});
return tfa;
}
}

View File

@ -18,6 +18,8 @@ export default Stacker(GetClientApiAuthMiddleware(), async (req: Request, res) =
email: mail.mail,
username: req.user.username,
displayName: req.user.name,
"display-name": req.user.name,
displayNameClaim: req.user.name,
name: req.user.name,
});
})

View File

@ -33,7 +33,7 @@ BackupCodeRoute.post(
console.log(codes);
let twofactor = TwoFactor.new(<IBackupCode>{
user: req.user._id,
type: TwoFATypes.OTC,
type: TwoFATypes.TOTP,
valid: true,
data: codes,
name: "",
@ -60,7 +60,7 @@ BackupCodeRoute.put(
!twofactor ||
!twofactor.valid ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC
twofactor.type !== TwoFATypes.TOTP
) {
throw new RequestError(
"Invalid Method!",

View File

@ -28,7 +28,7 @@ OTCRoute.post(
});
let twofactor = TwoFactor.new(<IOTC>{
user: req.user._id,
type: TwoFATypes.OTC,
type: TwoFATypes.TOTP,
valid: false,
data: secret.base32,
});
@ -49,7 +49,7 @@ OTCRoute.post(
if (
!twofactor ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC ||
twofactor.type !== TwoFATypes.TOTP ||
!twofactor.data ||
twofactor.valid
) {
@ -96,7 +96,7 @@ OTCRoute.put(
!twofactor ||
!twofactor.valid ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.OTC
twofactor.type !== TwoFATypes.TOTP
) {
throw new RequestError(
"Invalid Method!",

View File

@ -27,7 +27,7 @@ U2FRoute.post(
let twofactor = TwoFactor.new(<IYubiKey>{
user: req.user._id,
type: TwoFATypes.U2F,
type: TwoFATypes.WEBAUTHN,
valid: false,
data: {
registration: registrationRequest,
@ -49,7 +49,7 @@ U2FRoute.post(
if (
!twofactor ||
!twofactor.user.equals(req.user._id) ||
twofactor.type !== TwoFATypes.U2F ||
twofactor.type !== TwoFATypes.WEBAUTHN ||
!twofactor.data.registration ||
twofactor.valid
) {
@ -95,7 +95,7 @@ U2FRoute.get(
let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({
user: req.user._id,
type: TwoFATypes.U2F,
type: TwoFATypes.WEBAUTHN,
valid: true,
});
@ -142,7 +142,7 @@ U2FRoute.put(
let { login, special } = req.token;
let twofactor: IYubiKey = await TwoFactor.findOne({
user: req.user._id,
type: TwoFATypes.U2F,
type: TwoFATypes.WEBAUTHN,
valid: true,
});

View File

@ -3,16 +3,16 @@ import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
import { ObjectID } from "bson";
export enum TFATypes {
OTC,
TOTP,
BACKUP_CODE,
U2F,
WEBAUTHN,
APP_ALLOW,
}
export const TFANames = new Map<TFATypes, string>();
TFANames.set(TFATypes.OTC, "Authenticator");
TFANames.set(TFATypes.TOTP, "Authenticator");
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
TFANames.set(TFATypes.U2F, "Security Key (U2F)");
TFANames.set(TFATypes.WEBAUTHN, "Security Key (WebAuthn)");
TFANames.set(TFATypes.APP_ALLOW, "App Push");
export interface ITwoFactor extends ModelDataBase {
@ -53,7 +53,7 @@ const TwoFactor = DB.addModel<ITwoFactor>({
name: "twofactor",
versions: [
{
migration: (e) => {},
migration: (e) => { },
schema: {
user: { type: ObjectID },
valid: { type: Boolean },

View File

@ -96,6 +96,7 @@ export default async function TestData() {
if (!t) {
t = TwoFactor.new({
user: u._id,
name: "Test OTP",
type: 0,
valid: true,
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",

View File

@ -49,7 +49,13 @@ ViewRouter.use(
ViewRouter.use(
"/user",
addCache,
ServeStatic(path.join(viewsv2_location, "user"), { cacheControl: false })
ServeStatic(path.join(viewsv2_location, "user"), { cacheControl: false, })
);
ViewRouter.use(
"/static",
addCache,
ServeStatic(path.join(viewsv2_location, "../static"), { cacheControl: false, })
);
ViewRouter.get("/code", (req, res) => {