From 7df7cf269799be9ad992554495f22d80f2766e56 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Sun, 25 Aug 2024 13:36:19 +0200 Subject: [PATCH] setup and invite --- .env.example | 10 +- README.md | 29 ++++- package.json | 2 + src/command/inviteCommand.ts | 12 ++ src/command/inviteCommandHandler.ts | 56 +++++++++ src/command/refreshCommandHandler.ts | 2 +- src/command/userCommand.ts | 2 + src/command/userCommandHandler.ts | 6 +- src/controller/authController.ts | 28 ----- src/controller/inviteController.ts | 159 ++++++++++++++++++++++++ src/controller/setupController.ts | 11 ++ src/data-source.ts | 8 +- src/entity/invite.ts | 22 ++++ src/entity/user.ts | 8 +- src/helpers/mailHelper.ts | 38 ++++++ src/helpers/parameterPassCheckHelper.ts | 28 +++++ src/middleware/allowSetup.ts | 12 ++ src/migrations/1724579024939-invite.ts | 18 +++ src/routes/auth.ts | 6 +- src/routes/index.ts | 7 +- src/routes/setup.ts | 28 +++++ src/service/inviteService.ts | 26 ++++ src/service/userService.ts | 40 ++++++ 23 files changed, 515 insertions(+), 43 deletions(-) create mode 100644 src/command/inviteCommand.ts create mode 100644 src/command/inviteCommandHandler.ts create mode 100644 src/controller/inviteController.ts create mode 100644 src/controller/setupController.ts create mode 100644 src/entity/invite.ts create mode 100644 src/helpers/mailHelper.ts create mode 100644 src/helpers/parameterPassCheckHelper.ts create mode 100644 src/middleware/allowSetup.ts create mode 100644 src/migrations/1724579024939-invite.ts create mode 100644 src/routes/setup.ts create mode 100644 src/service/inviteService.ts diff --git a/.env.example b/.env.example index 00c5572..3da7bc4 100644 --- a/.env.example +++ b/.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) \ No newline at end of file +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 \ No newline at end of file diff --git a/README.md b/README.md index d70acea..8e3a6cd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ # member-administration-server -Mitgliederverwaltung \ No newline at end of file +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 diff --git a/package.json b/package.json index 992b12a..7c7a252 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/command/inviteCommand.ts b/src/command/inviteCommand.ts new file mode 100644 index 0000000..4011c44 --- /dev/null +++ b/src/command/inviteCommand.ts @@ -0,0 +1,12 @@ +export interface CreateInviteCommand { + mail: string; + username: string; + firstname: string; + lastname: string; + secret: string; +} + +export interface DeleteInviteCommand { + token: string; + mail: string; +} diff --git a/src/command/inviteCommandHandler.ts b/src/command/inviteCommandHandler.ts new file mode 100644 index 0000000..5e991c9 --- /dev/null +++ b/src/command/inviteCommandHandler.ts @@ -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} + */ + static async create(createInvite: CreateInviteCommand): Promise { + 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} + */ + static async deleteByTokenAndMail(deleteInvite: DeleteInviteCommand): Promise { + 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"); + }); + } +} diff --git a/src/command/refreshCommandHandler.ts b/src/command/refreshCommandHandler.ts index 8f7d674..174445e 100644 --- a/src/command/refreshCommandHandler.ts +++ b/src/command/refreshCommandHandler.ts @@ -42,7 +42,7 @@ export default abstract class RefreshCommandHandler { /** * @description delete refresh by user and token * @param DeleteRefreshCommand - * @returns {Promise} + * @returns {Promise} */ static async deleteByToken(deleteRefresh: DeleteRefreshCommand): Promise { return await dataSource diff --git a/src/command/userCommand.ts b/src/command/userCommand.ts index e94b0a0..1941645 100644 --- a/src/command/userCommand.ts +++ b/src/command/userCommand.ts @@ -1,5 +1,7 @@ export interface CreateUserCommand { mail: string; username: string; + firstname: string; + lastname: string; secret: string; } diff --git a/src/command/userCommandHandler.ts b/src/command/userCommandHandler.ts index a77f579..271f823 100644 --- a/src/command/userCommandHandler.ts +++ b/src/command/userCommandHandler.ts @@ -7,9 +7,9 @@ export default abstract class UserCommandHandler { /** * @description create user * @param CreateUserCommand - * @returns {Promise} + * @returns {Promise} */ - static async create(createUser: CreateUserCommand): Promise { + static async create(createUser: CreateUserCommand): Promise { 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() diff --git a/src/controller/authController.ts b/src/controller/authController.ts index d89c4aa..b77d9ba 100644 --- a/src/controller/authController.ts +++ b/src/controller/authController.ts @@ -132,31 +132,3 @@ export async function refresh(req: Request, res: Response): Promise { 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 { - // 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"); - }); -} diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts new file mode 100644 index 0000000..9613cbf --- /dev/null +++ b/src/controller/inviteController.ts @@ -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 { + 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 { + 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 { + 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, + }); +} diff --git a/src/controller/setupController.ts b/src/controller/setupController.ts new file mode 100644 index 0000000..f71194f --- /dev/null +++ b/src/controller/setupController.ts @@ -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 { + res.sendStatus(204); +} diff --git a/src/data-source.ts b/src/data-source.ts index 7200e94..aa9c708 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -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: [], diff --git a/src/entity/invite.ts b/src/entity/invite.ts new file mode 100644 index 0000000..654bf74 --- /dev/null +++ b/src/entity/invite.ts @@ -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; +} diff --git a/src/entity/user.ts b/src/entity/user.ts index a0da3db..5541468 100644 --- a/src/entity/user.ts +++ b/src/entity/user.ts @@ -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; } diff --git a/src/helpers/mailHelper.ts b/src/helpers/mailHelper.ts new file mode 100644 index 0000000..971078b --- /dev/null +++ b/src/helpers/mailHelper.ts @@ -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 { + 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)); + }); + } +} diff --git a/src/helpers/parameterPassCheckHelper.ts b/src/helpers/parameterPassCheckHelper.ts new file mode 100644 index 0000000..16ba5f0 --- /dev/null +++ b/src/helpers/parameterPassCheckHelper.ts @@ -0,0 +1,28 @@ +import { Request, Response } from "express"; +import BadRequestException from "../exceptions/badRequestException"; + +export default class ParamaterPassCheckHelper { + static requiredIncluded(testfor: Array, 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, 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): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + this.requiredIncluded(testfor, req.body); + next(); + }; + } + + static forbiddenIncludedMiddleware(testfor: Array): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + this.requiredIncluded(testfor, req.body); + next(); + }; + } +} diff --git a/src/middleware/allowSetup.ts b/src/middleware/allowSetup.ts new file mode 100644 index 0000000..e2306bf --- /dev/null +++ b/src/middleware/allowSetup.ts @@ -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(); +} diff --git a/src/migrations/1724579024939-invite.ts b/src/migrations/1724579024939-invite.ts new file mode 100644 index 0000000..220e37b --- /dev/null +++ b/src/migrations/1724579024939-invite.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Invite1724579024939 implements MigrationInterface { + name = 'Invite1724579024939' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`lastname\``); + await queryRunner.query(`ALTER TABLE \`user\` DROP COLUMN \`firstname\``); + await queryRunner.query(`DROP TABLE \`invite\``); + } + +} diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 7630077..b1200bc 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -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; diff --git a/src/routes/index.ts b/src/routes/index.ts index 6b250f9..ccbb953 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -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) => { diff --git a/src/routes/setup.ts b/src/routes/setup.ts new file mode 100644 index 0000000..9f228ae --- /dev/null +++ b/src/routes/setup.ts @@ -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; diff --git a/src/service/inviteService.ts b/src/service/inviteService.ts new file mode 100644 index 0000000..455df13 --- /dev/null +++ b/src/service/inviteService.ts @@ -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} + */ + static async getByMailAndToken(mail: string, token: string): Promise { + 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"); + }); + } +} diff --git a/src/service/userService.ts b/src/service/userService.ts index 2603925..82060c2 100644 --- a/src/service/userService.ts +++ b/src/service/userService.ts @@ -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>} + */ + static async getByMailOrUsername(mail?: string, username?: string): Promise> { + 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} + */ + static async count(): Promise { + return await dataSource + .getRepository(user) + .createQueryBuilder("user") + .select() + .getCount() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("could not count users"); + }); + } }