setup and invite
This commit is contained in:
parent
03e0f90279
commit
7df7cf2697
23 changed files with 515 additions and 43 deletions
10
.env.example
10
.env.example
|
@ -7,4 +7,12 @@ SERVER_PORT = portnumber
|
|||
|
||||
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
|
||||
JWT_EXPIRATION = [0-9]*(y|d|h|m|s)
|
||||
REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s)
|
||||
REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s)
|
||||
|
||||
MAIL_USERNAME = mail_username
|
||||
MAIL_PASSWORD = mail_password
|
||||
MAIL_HOST = mail_hoststring
|
||||
MAIL_PORT = mail_portnumber
|
||||
MAIL_SECURE (true|false) // true for port 465, fals for other ports
|
||||
|
||||
CLUB_NAME = clubname
|
29
README.md
29
README.md
|
@ -1,3 +1,30 @@
|
|||
# member-administration-server
|
||||
|
||||
Mitgliederverwaltung
|
||||
Memberadministration
|
||||
|
||||
Authentications is realized via JWT-Tokens. The server is able to send Mails to the members.
|
||||
Login is possible via Username and TOTP.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
1. MySql Database
|
||||
2. Access to the internet for sending Mails
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Copy the .env.example file to .env and fill in the required information
|
||||
2. Create a new Database in MySql named as in the .env file
|
||||
3. Install all packages via `npm install`
|
||||
4. Start the application to create the database schema
|
||||
|
||||
## Testing
|
||||
|
||||
1. Install the database-system-package you like (e.g. mysql, mariadb, postgresql, sqlite3)
|
||||
2. Configure type inside src/data-source.ts to run the database-system you like.
|
||||
3. Set migrationsRun to false and synchronize to true. (Migrations are build to suit mysql)
|
||||
4. Building the schema via CLI:
|
||||
- Run `npm run update-database` to build the schema using the migrations without starting the application
|
||||
- Run `npm run synchronize-database` to build the schema without using migrations without starting the application
|
||||
5. Run `npm run dev` to run inside dev-environment
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
"start_ts": "ts-node src/index.ts",
|
||||
"typeorm": "typeorm-ts-node-commonjs",
|
||||
"migrate": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:generate ./src/migrations/%npm_config_name% -d ./src/data-source.ts",
|
||||
"synchronize-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs schema:sync -d ./src/data-source.ts",
|
||||
"update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts",
|
||||
"revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts",
|
||||
"build": "tsc",
|
||||
"start": "node .",
|
||||
"dev": "npm run build && set NODE_ENV=development && npm run start"
|
||||
|
|
12
src/command/inviteCommand.ts
Normal file
12
src/command/inviteCommand.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface CreateInviteCommand {
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface DeleteInviteCommand {
|
||||
token: string;
|
||||
mail: string;
|
||||
}
|
56
src/command/inviteCommandHandler.ts
Normal file
56
src/command/inviteCommandHandler.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { invite } from "../entity/invite";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import { StringHelper } from "../helpers/stringHelper";
|
||||
import { CreateInviteCommand, DeleteInviteCommand } from "./inviteCommand";
|
||||
|
||||
export default abstract class InviteCommandHandler {
|
||||
/**
|
||||
* @description create user
|
||||
* @param CreateInviteCommand
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async create(createInvite: CreateInviteCommand): Promise<string> {
|
||||
const token = StringHelper.random(32);
|
||||
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(invite)
|
||||
.values({
|
||||
mail: createInvite.mail,
|
||||
token: token,
|
||||
username: createInvite.username,
|
||||
firstname: createInvite.firstname,
|
||||
lastname: createInvite.lastname,
|
||||
secret: createInvite.secret,
|
||||
})
|
||||
.orUpdate(["firstName", "lastName", "token", "secret"], ["mail"])
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return token;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed saving invite");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete invite by mail and token
|
||||
* @param DeleteRefreshCommand
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByTokenAndMail(deleteInvite: DeleteInviteCommand): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(invite)
|
||||
.where("invite.token = :token", { token: deleteInvite.token })
|
||||
.andWhere("invite.mail = :mail", { mail: deleteInvite.mail })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new InternalException("failed invite removal");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ export default abstract class RefreshCommandHandler {
|
|||
/**
|
||||
* @description delete refresh by user and token
|
||||
* @param DeleteRefreshCommand
|
||||
* @returns {Promise<refresh>}
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByToken(deleteRefresh: DeleteRefreshCommand): Promise<any> {
|
||||
return await dataSource
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export interface CreateUserCommand {
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
secret: string;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ export default abstract class UserCommandHandler {
|
|||
/**
|
||||
* @description create user
|
||||
* @param CreateUserCommand
|
||||
* @returns {Promise<string>}
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async create(createUser: CreateUserCommand): Promise<string> {
|
||||
static async create(createUser: CreateUserCommand): Promise<number> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
|
@ -17,6 +17,8 @@ export default abstract class UserCommandHandler {
|
|||
.values({
|
||||
username: createUser.username,
|
||||
mail: createUser.mail,
|
||||
firstname: createUser.firstname,
|
||||
lastname: createUser.lastname,
|
||||
secret: createUser.secret,
|
||||
})
|
||||
.execute()
|
||||
|
|
|
@ -132,31 +132,3 @@ export async function refresh(req: Request, res: Response): Promise<any> {
|
|||
refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description register new user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function register(req: Request, res: Response): Promise<any> {
|
||||
// TODO: change to invitation only
|
||||
let username = req.body.username;
|
||||
let mail = req.body.mail;
|
||||
var secret = speakeasy.generateSecret({ length: 20, name: "Mitgliederverwaltung" });
|
||||
|
||||
let createUser: CreateUserCommand = {
|
||||
username: username,
|
||||
mail: mail,
|
||||
secret: secret.base32,
|
||||
};
|
||||
await UserCommandHandler.create(createUser);
|
||||
|
||||
QRCode.toDataURL(secret.otpauth_url)
|
||||
.then((result) => {
|
||||
res.send(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("QRCode not created");
|
||||
});
|
||||
}
|
||||
|
|
159
src/controller/inviteController.ts
Normal file
159
src/controller/inviteController.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
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 } from "../command/refreshCommand";
|
||||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
import QRCode from "qrcode";
|
||||
import { CreateUserCommand } from "../command/userCommand";
|
||||
import UserCommandHandler from "../command/userCommandHandler";
|
||||
import { CreateInviteCommand, DeleteInviteCommand } from "../command/inviteCommand";
|
||||
import InviteCommandHandler from "../command/inviteCommandHandler";
|
||||
import MailHelper from "../helpers/mailHelper";
|
||||
import InviteService from "../service/inviteService";
|
||||
import UserService from "../service/userService";
|
||||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
|
||||
/**
|
||||
* @description start first user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function inviteUser(req: Request, res: Response): Promise<any> {
|
||||
let origin = req.headers.origin;
|
||||
let username = req.body.username;
|
||||
let mail = req.body.mail;
|
||||
let firstname = req.body.firstname;
|
||||
let lastname = req.body.lastname;
|
||||
|
||||
let users = await UserService.getByMailOrUsername(mail, username);
|
||||
if (users.length == 1) {
|
||||
// username or mail is used
|
||||
if (users[0].username == username && users[0].mail == mail) {
|
||||
throw new CustomRequestException(409, "Username and Mail are already in use");
|
||||
} else if (users[0].username == username) {
|
||||
throw new CustomRequestException(409, "Username is already in use");
|
||||
} else {
|
||||
throw new CustomRequestException(409, "Mail is already in use");
|
||||
}
|
||||
} else if (users.length >= 2) {
|
||||
throw new CustomRequestException(409, "Username and Mail are already in use");
|
||||
}
|
||||
|
||||
var secret = speakeasy.generateSecret({ length: 20, name: `Mitgliederverwaltung ${process.env.CLUB_NAME}` });
|
||||
|
||||
let createInvite: CreateInviteCommand = {
|
||||
username: username,
|
||||
mail: mail,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
secret: secret.base32,
|
||||
};
|
||||
let token = await InviteCommandHandler.create(createInvite);
|
||||
|
||||
// sendmail
|
||||
let mailhelper = new MailHelper();
|
||||
await mailhelper.sendMail(
|
||||
mail,
|
||||
`Email Bestätigung für Mitglieder Admin-Portal von ${process.env.CLUB_NAME}`,
|
||||
`Öffne folgenden Link: ${origin}/setup/verify?mail=${mail}&token=${token}`
|
||||
);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create first user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function verifyInvite(req: Request, res: Response): Promise<any> {
|
||||
let mail = req.body.mail;
|
||||
let token = req.body.token;
|
||||
|
||||
let { secret } = await InviteService.getByMailAndToken(mail, token);
|
||||
|
||||
const url = `otpauth://totp/Mitgliederverwaltung ${process.env.CLUB_NAME}?secret=${secret}`;
|
||||
|
||||
QRCode.toDataURL(url)
|
||||
.then((result) => {
|
||||
res.send(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("QRCode not created");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create first user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function finishInvite(req: Request, res: Response): Promise<any> {
|
||||
let mail = req.body.mail;
|
||||
let token = req.body.token;
|
||||
let totp = req.body.totp;
|
||||
|
||||
let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token);
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
|
||||
console.log(valid);
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
}
|
||||
|
||||
let createUser: CreateUserCommand = {
|
||||
username: username,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
mail: mail,
|
||||
secret: secret,
|
||||
};
|
||||
let id = await UserCommandHandler.create(createUser);
|
||||
|
||||
let jwtData: JWTToken = {
|
||||
userId: id,
|
||||
username: username,
|
||||
rights: [],
|
||||
};
|
||||
|
||||
let accessToken: string;
|
||||
let refreshToken: string;
|
||||
|
||||
JWTHelper.create(jwtData)
|
||||
.then((result) => {
|
||||
accessToken = result;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
throw new InternalException("Failed accessToken creation");
|
||||
});
|
||||
|
||||
let refreshCommand: CreateRefreshCommand = {
|
||||
userId: id,
|
||||
};
|
||||
refreshToken = await RefreshCommandHandler.create(refreshCommand);
|
||||
|
||||
let deleteInvite: DeleteInviteCommand = {
|
||||
mail: mail,
|
||||
token: token,
|
||||
};
|
||||
await InviteCommandHandler.deleteByTokenAndMail(deleteInvite);
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
}
|
11
src/controller/setupController.ts
Normal file
11
src/controller/setupController.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* @description Service is currently not configured
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function isSetup(req: Request, res: Response): Promise<any> {
|
||||
res.sendStatus(204);
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import "dotenv/config";
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
import { user } from "./entity/user";
|
||||
import { refresh } from "./entity/refresh";
|
||||
import { invite } from "./entity/invite";
|
||||
|
||||
import { Initial1724317398939 } from "./migrations/1724317398939-initial";
|
||||
import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange";
|
||||
import { Invite1724579024939 } from "./migrations/1724579024939-invite";
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: "mysql",
|
||||
|
@ -16,8 +20,8 @@ const dataSource = new DataSource({
|
|||
synchronize: false,
|
||||
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
||||
bigNumberStrings: false,
|
||||
entities: [user, refresh],
|
||||
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851],
|
||||
entities: [user, refresh, invite],
|
||||
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939],
|
||||
migrationsRun: true,
|
||||
migrationsTransactionMode: "each",
|
||||
subscribers: [],
|
||||
|
|
22
src/entity/invite.ts
Normal file
22
src/entity/invite.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class invite {
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
mail: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
token: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
firstname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
lastname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
secret: string;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
import { refresh } from "./refresh";
|
||||
|
||||
@Entity()
|
||||
|
@ -12,6 +12,12 @@ export class user {
|
|||
@Column({ type: "varchar", length: 255 })
|
||||
username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
firstname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
lastname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
secret: string;
|
||||
}
|
||||
|
|
38
src/helpers/mailHelper.ts
Normal file
38
src/helpers/mailHelper.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Transporter, createTransport, TransportOptions } from "nodemailer";
|
||||
|
||||
export default class MailHelper {
|
||||
private readonly transporter: Transporter;
|
||||
|
||||
constructor() {
|
||||
this.transporter = createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: Number(process.env.MAIL_PORT),
|
||||
secure: (process.env.MAIL_SECURE as "true" | "false") == "true",
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
} as TransportOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description send mail
|
||||
* @param {string} target
|
||||
* @param {string} subject
|
||||
* @param {string} content
|
||||
* @returns {Prmose<*>}
|
||||
*/
|
||||
async sendMail(target: string, subject: string, content: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.transporter
|
||||
.sendMail({
|
||||
from: `"${process.env.CLUB_NAME}" <${process.env.MAIL_USERNAME}>`,
|
||||
to: target,
|
||||
subject,
|
||||
text: content,
|
||||
})
|
||||
.then((info) => resolve(info.messageId))
|
||||
.catch((e) => reject(e));
|
||||
});
|
||||
}
|
||||
}
|
28
src/helpers/parameterPassCheckHelper.ts
Normal file
28
src/helpers/parameterPassCheckHelper.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Request, Response } from "express";
|
||||
import BadRequestException from "../exceptions/badRequestException";
|
||||
|
||||
export default class ParamaterPassCheckHelper {
|
||||
static requiredIncluded(testfor: Array<string>, obj: object) {
|
||||
let result = testfor.every((key) => Object.keys(obj).includes(key));
|
||||
if (!result) throw new BadRequestException(`not all required parameters included: ${testfor.join(",")}`);
|
||||
}
|
||||
|
||||
static forbiddenIncluded(testfor: Array<string>, obj: object) {
|
||||
let result = testfor.some((key) => Object.keys(obj).includes(key));
|
||||
if (!result) throw new BadRequestException(`PPC: forbidden parameters included: ${testfor.join(",")}`);
|
||||
}
|
||||
|
||||
static requiredIncludedMiddleware(testfor: Array<string>): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
this.requiredIncluded(testfor, req.body);
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
static forbiddenIncludedMiddleware(testfor: Array<string>): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
this.requiredIncluded(testfor, req.body);
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
12
src/middleware/allowSetup.ts
Normal file
12
src/middleware/allowSetup.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Request, Response } from "express";
|
||||
import UserService from "../service/userService";
|
||||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
|
||||
export default async function allowSetup(req: Request, res: Response, next: Function) {
|
||||
let count = await UserService.count();
|
||||
if (count != 0) {
|
||||
throw new CustomRequestException(405, "service is already set up");
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
18
src/migrations/1724579024939-invite.ts
Normal file
18
src/migrations/1724579024939-invite.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Invite1724579024939 implements MigrationInterface {
|
||||
name = 'Invite1724579024939'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE \`invite\` (\`mail\` varchar(255) NOT NULL, \`token\` varchar(255) NOT NULL, \`username\` varchar(255) NOT NULL, \`firstname\` varchar(255) NOT NULL, \`lastname\` varchar(255) NOT NULL, \`secret\` varchar(255) NOT NULL, PRIMARY KEY (\`mail\`)) ENGINE=InnoDB`);
|
||||
await queryRunner.query(`ALTER TABLE \`user\` ADD \`firstname\` varchar(255) NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE \`user\` ADD \`lastname\` varchar(255) NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`lastname\``);
|
||||
await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`firstname\``);
|
||||
await queryRunner.query(`DROP TABLE \`invite\``);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import express from "express";
|
||||
import { login, logout, refresh, register } from "../controller/authController";
|
||||
import { login, logout, refresh } from "../controller/authController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
|
@ -15,8 +15,4 @@ router.post("/refresh", async (req, res) => {
|
|||
await refresh(req, res);
|
||||
});
|
||||
|
||||
router.post("/register", async (req, res) => {
|
||||
await register(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import express from "express";
|
||||
import type { Express } from "express";
|
||||
import cors from "cors";
|
||||
|
||||
import type { Express } from "express";
|
||||
import errorHandler from "../middleware/errorHandler";
|
||||
import allowSetup from "../middleware/allowSetup";
|
||||
import authenticate from "../middleware/authenticate";
|
||||
import errorHandler from "../middleware/errorHandler";
|
||||
|
||||
import setup from "./setup";
|
||||
import auth from "./auth";
|
||||
|
||||
export default (app: Express) => {
|
||||
|
@ -18,6 +20,7 @@ export default (app: Express) => {
|
|||
app.use(cors());
|
||||
app.options("*", cors());
|
||||
|
||||
app.use("/setup", allowSetup, setup);
|
||||
app.use("/auth", auth);
|
||||
app.use(authenticate);
|
||||
app.use("/secured", (req, res) => {
|
||||
|
|
28
src/routes/setup.ts
Normal file
28
src/routes/setup.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import express from "express";
|
||||
import { isSetup } from "../controller/setupController";
|
||||
import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
|
||||
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
await isSetup(req, res);
|
||||
});
|
||||
|
||||
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
|
||||
await verifyInvite(req, res);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]),
|
||||
async (req, res) => {
|
||||
await inviteUser(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishInvite(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
26
src/service/inviteService.ts
Normal file
26
src/service/inviteService.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { invite } from "../entity/invite";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
|
||||
export default abstract class InviteService {
|
||||
/**
|
||||
* @description get invite by id
|
||||
* @param mail string
|
||||
* @param token string
|
||||
* @returns {Promise<invite>}
|
||||
*/
|
||||
static async getByMailAndToken(mail: string, token: string): Promise<invite> {
|
||||
return await dataSource
|
||||
.getRepository(invite)
|
||||
.createQueryBuilder("invite")
|
||||
.where("invite.mail = :mail", { mail: mail })
|
||||
.andWhere("invite.token = :token", { token: token })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("invite not found by mail and token");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -41,4 +41,44 @@ export default abstract class UserService {
|
|||
throw new InternalException("user not found by username");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get users by mail or username
|
||||
* @param username string
|
||||
* @param mail string
|
||||
* @returns {Promise<Array<user>>}
|
||||
*/
|
||||
static async getByMailOrUsername(mail?: string, username?: string): Promise<Array<user>> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.select()
|
||||
.where("user.mail = :mail", { mail: mail })
|
||||
.orWhere("user.username = :username", { username: username })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("user not found by mail or username");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get count of users
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async count(): Promise<number> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.select()
|
||||
.getCount()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("could not count users");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue