Start implementing a new user page for account and security settings
This commit is contained in:
@ -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",
|
||||
|
@ -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);
|
||||
|
||||
|
49
Backend/src/api/jrpc/account_service.ts
Normal file
49
Backend/src/api/jrpc/account_service.ts
Normal 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;
|
||||
}
|
||||
}
|
45
Backend/src/api/jrpc/index.ts
Normal file
45
Backend/src/api/jrpc/index.ts
Normal 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;
|
71
Backend/src/api/jrpc/security_service.ts
Normal file
71
Backend/src/api/jrpc/security_service.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
})
|
||||
|
@ -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!",
|
||||
|
@ -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!",
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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 },
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
|
Reference in New Issue
Block a user