login and authentication
login via totp authentication via access and refresh tokens
This commit is contained in:
parent
6696975bee
commit
e1ec65350d
28 changed files with 3750 additions and 0 deletions
10
.env.example
Normal file
10
.env.example
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
DB_HOST = database_host
|
||||||
|
DB_NAME = database_name
|
||||||
|
DB_USERNAME = database_username
|
||||||
|
DB_PASSWORD = database_password
|
||||||
|
|
||||||
|
SERVER_PORT = portnumber
|
||||||
|
|
||||||
|
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
|
||||||
|
JWT_EXPIRATION = [0-9]*(y|d|h|m|s)
|
||||||
|
REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s)
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
3125
package-lock.json
generated
Normal file
3125
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
55
package.json
Normal file
55
package.json
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"name": "member-administration-server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Feuerwehr/Verein Mitgliederverwaltung Server",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"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",
|
||||||
|
"update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node .",
|
||||||
|
"dev": "npm run build && set NODE_ENV=development && npm run start"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Feuerwehr"
|
||||||
|
],
|
||||||
|
"author": "JK Effects",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^5.0.0-beta.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"nodemailer": "^6.9.14",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"socket.io": "^4.7.5",
|
||||||
|
"speakeasy": "^2.0.0",
|
||||||
|
"typeorm": "^0.3.20",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.14",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/ms": "^0.7.34",
|
||||||
|
"@types/mysql": "^2.15.21",
|
||||||
|
"@types/node": "^16.18.41",
|
||||||
|
"@types/node-schedule": "^2.1.6",
|
||||||
|
"@types/nodemailer": "^6.4.14",
|
||||||
|
"@types/qrcode": "~1.5.5",
|
||||||
|
"@types/speakeasy": "^2.0.10",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
|
"ts-node": "10.7.0",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
}
|
||||||
|
}
|
8
src/command/refreshCommand.ts
Normal file
8
src/command/refreshCommand.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export interface CreateRefreshCommand {
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteRefreshCommand {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
39
src/command/refreshCommandHandler.ts
Normal file
39
src/command/refreshCommandHandler.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { dataSource } from "../data-source";
|
||||||
|
import { refresh } from "../entity/refresh";
|
||||||
|
import InternalException from "../exceptions/internalException";
|
||||||
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
|
import UserService from "../service/userService";
|
||||||
|
import { JWTRefresh } from "../type/jwtTypes";
|
||||||
|
import { CreateRefreshCommand } from "./refreshCommand";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
|
export default abstract class RefreshCommandHandler {
|
||||||
|
/**
|
||||||
|
* @description create and save refreshToken to user
|
||||||
|
* @param CreateRefreshCommand
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static async create(createRefresh: CreateRefreshCommand): Promise<string> {
|
||||||
|
let createRefreshToken: JWTRefresh = {
|
||||||
|
userId: createRefresh.userId,
|
||||||
|
};
|
||||||
|
const refreshToken = await JWTHelper.create(createRefreshToken);
|
||||||
|
|
||||||
|
return await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(refresh)
|
||||||
|
.values({
|
||||||
|
token: refreshToken,
|
||||||
|
user: await UserService.getById(createRefresh.userId),
|
||||||
|
expiry: ms(process.env.REFRESH_EXPIRATION),
|
||||||
|
})
|
||||||
|
.execute()
|
||||||
|
.then((result) => {
|
||||||
|
return refreshToken;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("Failed saving refresh token");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
5
src/command/userCommand.ts
Normal file
5
src/command/userCommand.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export interface CreateUserCommand {
|
||||||
|
mail: string;
|
||||||
|
username: string;
|
||||||
|
secret: string;
|
||||||
|
}
|
30
src/command/userCommandHandler.ts
Normal file
30
src/command/userCommandHandler.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { dataSource } from "../data-source";
|
||||||
|
import { user } from "../entity/user";
|
||||||
|
import InternalException from "../exceptions/internalException";
|
||||||
|
import { CreateUserCommand } from "./userCommand";
|
||||||
|
|
||||||
|
export default abstract class UserCommandHandler {
|
||||||
|
/**
|
||||||
|
* @description create user
|
||||||
|
* @param CreateUserCommand
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
static async create(createUser: CreateUserCommand): Promise<string> {
|
||||||
|
return await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(user)
|
||||||
|
.values({
|
||||||
|
username: createUser.username,
|
||||||
|
mail: createUser.mail,
|
||||||
|
secret: createUser.secret,
|
||||||
|
})
|
||||||
|
.execute()
|
||||||
|
.then((result) => {
|
||||||
|
return result.identifiers[0].id;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("Failed saving user");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
111
src/controller/authController.ts
Normal file
111
src/controller/authController.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
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 UserService from "../service/userService";
|
||||||
|
import speakeasy from "speakeasy";
|
||||||
|
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
import { CreateUserCommand } from "../command/userCommand";
|
||||||
|
import UserCommandHandler from "../command/userCommandHandler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Check authentication status by token
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function login(req: Request, res: Response): Promise<any> {
|
||||||
|
let username = req.body.username;
|
||||||
|
let totp = req.body.totp;
|
||||||
|
|
||||||
|
let { id, secret } = await UserService.getByUsername(username);
|
||||||
|
|
||||||
|
let valid = speakeasy.totp.verify({
|
||||||
|
secret: secret,
|
||||||
|
encoding: "base32",
|
||||||
|
token: totp,
|
||||||
|
window: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description logout user by token (invalidate refresh token)
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function logout(req: Request, res: Response): Promise<any> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description refresh expired token
|
||||||
|
* @param req {Request} Express req object
|
||||||
|
* @param res {Response} Express res object
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
export async function refresh(req: Request, res: Response): Promise<any> {
|
||||||
|
let token = req.body.token;
|
||||||
|
let refresh = req.body.refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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");
|
||||||
|
});
|
||||||
|
}
|
25
src/data-source.ts
Normal file
25
src/data-source.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import "dotenv/config";
|
||||||
|
import "reflect-metadata";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { user } from "./entity/user";
|
||||||
|
import { refresh } from "./entity/refresh";
|
||||||
|
import { Initial1724317398939 } from "./migrations/1724317398939-initial";
|
||||||
|
|
||||||
|
const dataSource = new DataSource({
|
||||||
|
type: "mysql",
|
||||||
|
host: process.env.NODE_ENV || process.env.DBMODE ? "localhost" : process.env.DB_HOST,
|
||||||
|
port: 3306,
|
||||||
|
username: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
synchronize: false,
|
||||||
|
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
||||||
|
bigNumberStrings: false,
|
||||||
|
entities: [user, refresh],
|
||||||
|
migrations: [Initial1724317398939],
|
||||||
|
migrationsRun: true,
|
||||||
|
migrationsTransactionMode: "each",
|
||||||
|
subscribers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export { dataSource };
|
17
src/entity/refresh.ts
Normal file
17
src/entity/refresh.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||||
|
import { user } from "./user";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class refresh {
|
||||||
|
@PrimaryColumn({ generated: "increment", type: "int" })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255 })
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
@Column({ type: "datetime" })
|
||||||
|
expiry: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => user)
|
||||||
|
user: user;
|
||||||
|
}
|
17
src/entity/user.ts
Normal file
17
src/entity/user.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
|
||||||
|
import { refresh } from "./refresh";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class user {
|
||||||
|
@PrimaryColumn({ generated: "increment", type: "int" })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255 })
|
||||||
|
mail: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255 })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255 })
|
||||||
|
secret: string;
|
||||||
|
}
|
9
src/exceptions/badRequestException.ts
Normal file
9
src/exceptions/badRequestException.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { ExceptionBase } from "./exceptionsBaseType";
|
||||||
|
|
||||||
|
export default class BadRequestException extends Error implements ExceptionBase {
|
||||||
|
statusCode: number = 400;
|
||||||
|
|
||||||
|
constructor(msg: string) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
10
src/exceptions/customRequestException.ts
Normal file
10
src/exceptions/customRequestException.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { ExceptionBase } from "./exceptionsBaseType";
|
||||||
|
|
||||||
|
export default class CustomRequestException extends Error implements ExceptionBase {
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
constructor(status: number, msg: string) {
|
||||||
|
super(msg);
|
||||||
|
this.statusCode = status;
|
||||||
|
}
|
||||||
|
}
|
3
src/exceptions/exceptionsBaseType.ts
Normal file
3
src/exceptions/exceptionsBaseType.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export type ExceptionBase = {
|
||||||
|
statusCode: number;
|
||||||
|
} & Error;
|
9
src/exceptions/internalException.ts
Normal file
9
src/exceptions/internalException.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { ExceptionBase } from "./exceptionsBaseType";
|
||||||
|
|
||||||
|
export default class InternalException extends Error implements ExceptionBase {
|
||||||
|
statusCode: number = 500;
|
||||||
|
|
||||||
|
constructor(msg: string) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
9
src/exceptions/unauthorizedRequestException.ts
Normal file
9
src/exceptions/unauthorizedRequestException.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { ExceptionBase } from "./exceptionsBaseType";
|
||||||
|
|
||||||
|
export default class UnauthorizedRequestException extends Error implements ExceptionBase {
|
||||||
|
statusCode: number = 401;
|
||||||
|
|
||||||
|
constructor(msg: string) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
40
src/helpers/jwtHelper.ts
Normal file
40
src/helpers/jwtHelper.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { JWTData } from "../type/jwtTypes";
|
||||||
|
|
||||||
|
export abstract class JWTHelper {
|
||||||
|
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
||||||
|
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
||||||
|
if (err) reject(err.message);
|
||||||
|
else resolve(decoded);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(data: JWTData): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
jwt.sign(
|
||||||
|
data,
|
||||||
|
process.env.JWT_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: process.env.JWT_EXPIRATION,
|
||||||
|
},
|
||||||
|
(err, token) => {
|
||||||
|
if (err) reject(err.message);
|
||||||
|
else resolve(token);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static decode(token: string): Promise<string | jwt.JwtPayload> {
|
||||||
|
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let decoded = jwt.decode(token);
|
||||||
|
resolve(decoded);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
11
src/helpers/stringHelper.ts
Normal file
11
src/helpers/stringHelper.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export abstract class StringHelper {
|
||||||
|
static random(len: number, charSet?: string): string {
|
||||||
|
charSet = charSet || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
var randomString = "";
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
var randomPoz = Math.floor(Math.random() * charSet.length);
|
||||||
|
randomString += charSet.substring(randomPoz, randomPoz + 1);
|
||||||
|
}
|
||||||
|
return randomString;
|
||||||
|
}
|
||||||
|
}
|
23
src/index.ts
Normal file
23
src/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import "dotenv/config";
|
||||||
|
import express from "express";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
export interface Request {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
rights: Array<string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { dataSource } from "./data-source";
|
||||||
|
|
||||||
|
dataSource.initialize();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
import router from "./routes/index";
|
||||||
|
router(app);
|
||||||
|
app.listen(process.env.SERVER_PORT, () => {
|
||||||
|
console.log(`listening on *:${process.env.SERVER_PORT}`);
|
||||||
|
});
|
37
src/middleware/authenticate.ts
Normal file
37
src/middleware/authenticate.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import BadRequestException from "../exceptions/badRequestException";
|
||||||
|
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||||
|
import InternalException from "../exceptions/internalException";
|
||||||
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
|
|
||||||
|
export default async function authenticate(req: Request, res: Response, next: Function) {
|
||||||
|
const bearer = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!bearer) {
|
||||||
|
throw new BadRequestException("Provide Authorization Header");
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded: string | jwt.JwtPayload;
|
||||||
|
await JWTHelper.validate(bearer)
|
||||||
|
.then((result) => {
|
||||||
|
decoded = result;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err == "jwt expired") {
|
||||||
|
throw new UnauthorizedRequestException("Token expired");
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException("Failed Authorization Header decoding");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof decoded == "string" || !decoded) {
|
||||||
|
throw new InternalException("process failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
req.userId = decoded.userId;
|
||||||
|
req.username = decoded.username;
|
||||||
|
req.rights = decoded.rights;
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
9
src/middleware/errorHandler.ts
Normal file
9
src/middleware/errorHandler.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { ExceptionBase } from "../exceptions/exceptionsBaseType";
|
||||||
|
|
||||||
|
export default function errorHandler(err: ExceptionBase, req: Request, res: Response, next: Function) {
|
||||||
|
let status = err.statusCode ?? 500;
|
||||||
|
let msg = err.message;
|
||||||
|
|
||||||
|
res.status(status).send(msg);
|
||||||
|
}
|
18
src/migrations/1724317398939-initial.ts
Normal file
18
src/migrations/1724317398939-initial.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class Initial1724317398939 implements MigrationInterface {
|
||||||
|
name = 'Initial1724317398939'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE \`user\` (\`id\` int NOT NULL AUTO_INCREMENT, \`mail\` varchar(255) NOT NULL, \`username\` varchar(255) NOT NULL, \`secret\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||||
|
await queryRunner.query(`CREATE TABLE \`refresh\` (\`id\` int NOT NULL AUTO_INCREMENT, \`token\` varchar(255) NOT NULL, \`expiry\` datetime NOT NULL, \`userId\` int NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`refresh\` ADD CONSTRAINT \`FK_b39e4ed3bfa789758e476870ec2\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`refresh\` DROP FOREIGN KEY \`FK_b39e4ed3bfa789758e476870ec2\``);
|
||||||
|
await queryRunner.query(`DROP TABLE \`refresh\``);
|
||||||
|
await queryRunner.query(`DROP TABLE \`user\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
22
src/routes/auth.ts
Normal file
22
src/routes/auth.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import express from "express";
|
||||||
|
import { login, logout, refresh, register } from "../controller/authController";
|
||||||
|
|
||||||
|
var router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
|
router.post("/login", async (req, res) => {
|
||||||
|
await login(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/logout", async (req, res) => {
|
||||||
|
await logout(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/refresh", async (req, res) => {
|
||||||
|
await refresh(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/register", async (req, res) => {
|
||||||
|
await register(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
27
src/routes/index.ts
Normal file
27
src/routes/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import express from "express";
|
||||||
|
import cors from "cors";
|
||||||
|
|
||||||
|
import type { Express } from "express";
|
||||||
|
import errorHandler from "../middleware/errorHandler";
|
||||||
|
import authenticate from "../middleware/authenticate";
|
||||||
|
|
||||||
|
import auth from "./auth";
|
||||||
|
|
||||||
|
export default (app: Express) => {
|
||||||
|
app.set("query parser", "extended");
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(
|
||||||
|
express.urlencoded({
|
||||||
|
extended: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(cors());
|
||||||
|
app.options("*", cors());
|
||||||
|
|
||||||
|
app.use("/auth", auth);
|
||||||
|
app.use(authenticate);
|
||||||
|
app.use("/secured", (req, res) => {
|
||||||
|
res.send("hallo");
|
||||||
|
});
|
||||||
|
app.use(errorHandler);
|
||||||
|
};
|
44
src/service/userService.ts
Normal file
44
src/service/userService.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { dataSource } from "../data-source";
|
||||||
|
import { user } from "../entity/user";
|
||||||
|
import InternalException from "../exceptions/internalException";
|
||||||
|
|
||||||
|
export default abstract class UserService {
|
||||||
|
/**
|
||||||
|
* @description get user by id
|
||||||
|
* @param id number
|
||||||
|
* @returns {Promise<user>}
|
||||||
|
*/
|
||||||
|
static async getById(id: number): Promise<user> {
|
||||||
|
return await dataSource
|
||||||
|
.getRepository(user)
|
||||||
|
.createQueryBuilder("user")
|
||||||
|
.where("user.id = :id", { id: id })
|
||||||
|
.getOneOrFail()
|
||||||
|
.then((res) => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("user not found by id");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get user by username
|
||||||
|
* @param username string
|
||||||
|
* @returns {Promise<user>}
|
||||||
|
*/
|
||||||
|
static async getByUsername(username: string): Promise<user> {
|
||||||
|
return await dataSource
|
||||||
|
.getRepository(user)
|
||||||
|
.createQueryBuilder("user")
|
||||||
|
.select()
|
||||||
|
.where("user.username = :username", { username: username })
|
||||||
|
.getOneOrFail()
|
||||||
|
.then((res) => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("user not found by username");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
13
src/type/jwtTypes.ts
Normal file
13
src/type/jwtTypes.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export type JWTData = {
|
||||||
|
[key: string]: string | number | Array<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JWTToken = {
|
||||||
|
userId: number;
|
||||||
|
username: string;
|
||||||
|
rights: Array<string>;
|
||||||
|
} & JWTData;
|
||||||
|
|
||||||
|
export type JWTRefresh = {
|
||||||
|
userId: number;
|
||||||
|
} & JWTData;
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"*": ["node_modules/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
Loading…
Reference in a new issue