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_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
|
||||||
JWT_EXPIRATION = [0-9]*(y|d|h|m|s)
|
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
|
# 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",
|
"start_ts": "ts-node src/index.ts",
|
||||||
"typeorm": "typeorm-ts-node-commonjs",
|
"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",
|
"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",
|
"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",
|
"build": "tsc",
|
||||||
"start": "node .",
|
"start": "node .",
|
||||||
"dev": "npm run build && set NODE_ENV=development && npm run start"
|
"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
|
* @description delete refresh by user and token
|
||||||
* @param DeleteRefreshCommand
|
* @param DeleteRefreshCommand
|
||||||
* @returns {Promise<refresh>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
static async deleteByToken(deleteRefresh: DeleteRefreshCommand): Promise<any> {
|
static async deleteByToken(deleteRefresh: DeleteRefreshCommand): Promise<any> {
|
||||||
return await dataSource
|
return await dataSource
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export interface CreateUserCommand {
|
export interface CreateUserCommand {
|
||||||
mail: string;
|
mail: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ export default abstract class UserCommandHandler {
|
||||||
/**
|
/**
|
||||||
* @description create user
|
* @description create user
|
||||||
* @param CreateUserCommand
|
* @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
|
return await dataSource
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.insert()
|
.insert()
|
||||||
|
@ -17,6 +17,8 @@ export default abstract class UserCommandHandler {
|
||||||
.values({
|
.values({
|
||||||
username: createUser.username,
|
username: createUser.username,
|
||||||
mail: createUser.mail,
|
mail: createUser.mail,
|
||||||
|
firstname: createUser.firstname,
|
||||||
|
lastname: createUser.lastname,
|
||||||
secret: createUser.secret,
|
secret: createUser.secret,
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
|
|
|
@ -132,31 +132,3 @@ export async function refresh(req: Request, res: Response): Promise<any> {
|
||||||
refreshToken,
|
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 "dotenv/config";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
|
|
||||||
import { user } from "./entity/user";
|
import { user } from "./entity/user";
|
||||||
import { refresh } from "./entity/refresh";
|
import { refresh } from "./entity/refresh";
|
||||||
|
import { invite } from "./entity/invite";
|
||||||
|
|
||||||
import { Initial1724317398939 } from "./migrations/1724317398939-initial";
|
import { Initial1724317398939 } from "./migrations/1724317398939-initial";
|
||||||
import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange";
|
import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange";
|
||||||
|
import { Invite1724579024939 } from "./migrations/1724579024939-invite";
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: "mysql",
|
type: "mysql",
|
||||||
|
@ -16,8 +20,8 @@ const dataSource = new DataSource({
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
||||||
bigNumberStrings: false,
|
bigNumberStrings: false,
|
||||||
entities: [user, refresh],
|
entities: [user, refresh, invite],
|
||||||
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851],
|
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
subscribers: [],
|
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";
|
import { refresh } from "./refresh";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -12,6 +12,12 @@ export class user {
|
||||||
@Column({ type: "varchar", length: 255 })
|
@Column({ type: "varchar", length: 255 })
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255 })
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255 })
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 255 })
|
@Column({ type: "varchar", length: 255 })
|
||||||
secret: string;
|
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 express from "express";
|
||||||
import { login, logout, refresh, register } from "../controller/authController";
|
import { login, logout, refresh } from "../controller/authController";
|
||||||
|
|
||||||
var router = express.Router({ mergeParams: true });
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
@ -15,8 +15,4 @@ router.post("/refresh", async (req, res) => {
|
||||||
await refresh(req, res);
|
await refresh(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/register", async (req, res) => {
|
|
||||||
await register(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import type { Express } from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
|
||||||
import type { Express } from "express";
|
import allowSetup from "../middleware/allowSetup";
|
||||||
import errorHandler from "../middleware/errorHandler";
|
|
||||||
import authenticate from "../middleware/authenticate";
|
import authenticate from "../middleware/authenticate";
|
||||||
|
import errorHandler from "../middleware/errorHandler";
|
||||||
|
|
||||||
|
import setup from "./setup";
|
||||||
import auth from "./auth";
|
import auth from "./auth";
|
||||||
|
|
||||||
export default (app: Express) => {
|
export default (app: Express) => {
|
||||||
|
@ -18,6 +20,7 @@ export default (app: Express) => {
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.options("*", cors());
|
app.options("*", cors());
|
||||||
|
|
||||||
|
app.use("/setup", allowSetup, setup);
|
||||||
app.use("/auth", auth);
|
app.use("/auth", auth);
|
||||||
app.use(authenticate);
|
app.use(authenticate);
|
||||||
app.use("/secured", (req, res) => {
|
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");
|
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