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

@ -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,
});