diff --git a/package.json b/package.json index 335f5ce..afc485d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ ], "license": "CC-BY-NC-SA-4.0", "dependencies": { + "argon2": "^0.27.0", "body-parser": "^1.19.0", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", @@ -39,7 +40,8 @@ "routing-controllers-openapi": "^2.1.0", "swagger-ui-express": "^4.1.5", "typeorm": "^0.2.29", - "typeorm-routing-controllers-extensions": "^0.2.0" + "typeorm-routing-controllers-extensions": "^0.2.0", + "uuid": "^8.3.1" }, "devDependencies": { "@types/cors": "^2.8.8", @@ -49,6 +51,7 @@ "@types/multer": "^1.4.4", "@types/node": "^14.14.9", "@types/swagger-ui-express": "^4.1.2", + "@types/uuid": "^8.3.0", "dotenv-safe": "^8.2.0", "nodemon": "^2.0.6", "sqlite3": "^5.0.0", @@ -61,4 +64,4 @@ "build": "tsc", "docs": "typedoc --out docs src" } -} +} \ No newline at end of file diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts new file mode 100644 index 0000000..dbc4d3a --- /dev/null +++ b/src/controllers/UserController.ts @@ -0,0 +1,86 @@ +import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { EntityFromBody } from 'typeorm-routing-controllers-extensions'; +import { UserGroupNotFoundError, UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors'; +import { CreateUser } from '../models/creation/CreateUser'; +import { User } from '../models/entities/User'; + + +@JsonController('/users') +export class UserController { + private userRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.userRepository = getConnectionManager().get().getRepository(User); + } + + @Get() + @ResponseSchema(User, { isArray: true }) + @OpenAPI({ description: 'Lists all users.' }) + getAll() { + return this.userRepository.find(); + } + + @Get('/:id') + @ResponseSchema(User) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) + @OnUndefined(UserNotFoundError) + @OpenAPI({ description: 'Returns a user of a specified id (if it exists)' }) + getOne(@Param('id') id: number) { + return this.userRepository.findOne({ id: id }); + } + + @Post() + @ResponseSchema(User) + @ResponseSchema(UserGroupNotFoundError) + @OpenAPI({ description: 'Create a new user object (id will be generated automagicly).' }) + async post(@Body({ validate: true }) createUser: CreateUser) { + let user; + try { + user = await createUser.toUser(); + } catch (error) { + return error; + } + + return this.userRepository.save(user); + } + + @Put('/:id') + @ResponseSchema(User) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) + @ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update a user object (id can't be changed)." }) + async put(@Param('id') id: number, @EntityFromBody() user: User) { + let oldUser = await this.userRepository.findOne({ id: id }); + + if (!oldUser) { + throw new UserNotFoundError(); + } + + if (oldUser.id != user.id) { + throw new UserIdsNotMatchingError(); + } + + await this.userRepository.update(oldUser, user); + return user; + } + + @Delete('/:id') + @ResponseSchema(User) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Delete a specified runner (if it exists).' }) + async remove(@Param('id') id: number) { + let runner = await this.userRepository.findOne({ id: id }); + + if (!runner) { + throw new UserNotFoundError(); + } + + await this.userRepository.delete(runner); + return runner; + } +} diff --git a/src/controllers/UserGroupController.ts b/src/controllers/UserGroupController.ts new file mode 100644 index 0000000..fb158ed --- /dev/null +++ b/src/controllers/UserGroupController.ts @@ -0,0 +1,86 @@ +import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { EntityFromBody } from 'typeorm-routing-controllers-extensions'; +import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors'; +import { CreateUserGroup } from '../models/creation/CreateUserGroup'; +import { UserGroup } from '../models/entities/UserGroup'; + + +@JsonController('/usergroups') +export class UserGroupController { + private userGroupsRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.userGroupsRepository = getConnectionManager().get().getRepository(UserGroup); + } + + @Get() + @ResponseSchema(UserGroup, { isArray: true }) + @OpenAPI({ description: 'Lists all usergroups.' }) + getAll() { + return this.userGroupsRepository.find(); + } + + @Get('/:id') + @ResponseSchema(UserGroup) + @ResponseSchema(UserGroupNotFoundError, { statusCode: 404 }) + @OnUndefined(UserGroupNotFoundError) + @OpenAPI({ description: 'Returns a usergroup of a specified id (if it exists)' }) + getOne(@Param('id') id: number) { + return this.userGroupsRepository.findOne({ id: id }); + } + + @Post() + @ResponseSchema(UserGroup) + @ResponseSchema(UserGroupNotFoundError) + @OpenAPI({ description: 'Create a new usergroup object (id will be generated automagicly).' }) + async post(@Body({ validate: true }) createUserGroup: CreateUserGroup) { + let userGroup; + try { + userGroup = await createUserGroup.toUserGroup(); + } catch (error) { + return error; + } + + return this.userGroupsRepository.save(userGroup); + } + + @Put('/:id') + @ResponseSchema(UserGroup) + @ResponseSchema(UserGroupNotFoundError, { statusCode: 404 }) + @ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update a usergroup object (id can't be changed)." }) + async put(@Param('id') id: number, @EntityFromBody() userGroup: UserGroup) { + let oldUserGroup = await this.userGroupsRepository.findOne({ id: id }); + + if (!oldUserGroup) { + throw new UserGroupNotFoundError() + } + + if (oldUserGroup.id != userGroup.id) { + throw new UserGroupIdsNotMatchingError(); + } + + await this.userGroupsRepository.update(oldUserGroup, userGroup); + return userGroup; + } + + @Delete('/:id') + @ResponseSchema(UserGroup) + @ResponseSchema(UserGroupNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Delete a specified usergroup (if it exists).' }) + async remove(@Param('id') id: number) { + let userGroup = await this.userGroupsRepository.findOne({ id: id }); + + if (!userGroup) { + throw new UserGroupNotFoundError(); + } + + await this.userGroupsRepository.delete(userGroup); + return userGroup; + } +} diff --git a/src/errors/UserErrors.ts b/src/errors/UserErrors.ts new file mode 100644 index 0000000..4c9e98b --- /dev/null +++ b/src/errors/UserErrors.ts @@ -0,0 +1,47 @@ +import { IsString } from 'class-validator'; +import { NotAcceptableError, NotFoundError } from 'routing-controllers'; + +/** + * Error to throw when a usergroup couldn't be found. + */ +export class UserGroupNotFoundError extends NotFoundError { + @IsString() + name = "UserGroupNotFoundError" + + @IsString() + message = "User Group not found!" +} + +/** + * Error to throw when no username or email is set + */ +export class UsernameOrEmailNeededError extends NotFoundError { + @IsString() + name = "UsernameOrEmailNeededError" + + @IsString() + message = "no username or email is set!" +} + +/** + * Error to throw when a user couldn't be found. + */ +export class UserNotFoundError extends NotFoundError { + @IsString() + name = "UserNotFoundError" + + @IsString() + message = "User not found!" +} + +/** + * Error to throw when two users' ids don't match. + * Usually occurs when a user tries to change a user's id. + */ +export class UserIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "UserIdsNotMatchingError" + + @IsString() + message = "The id's don't match!! \n And if you wanted to change a user's id: This isn't allowed" +} \ No newline at end of file diff --git a/src/errors/UserGroupErrors.ts b/src/errors/UserGroupErrors.ts new file mode 100644 index 0000000..15495b9 --- /dev/null +++ b/src/errors/UserGroupErrors.ts @@ -0,0 +1,36 @@ +import { IsString } from 'class-validator'; +import { NotAcceptableError, NotFoundError } from 'routing-controllers'; + +/** + * Error to throw when no groupname is set + */ +export class GroupNameNeededError extends NotFoundError { + @IsString() + name = "GroupNameNeededError" + + @IsString() + message = "no groupname is set!" +} + +/** + * Error to throw when a usergroup couldn't be found. + */ +export class UserGroupNotFoundError extends NotFoundError { + @IsString() + name = "UserGroupNotFoundError" + + @IsString() + message = "User Group not found!" +} + +/** + * Error to throw when two usergroups' ids don't match. + * Usually occurs when a user tries to change a usergroups's id. + */ +export class UserGroupIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "UserGroupIdsNotMatchingError" + + @IsString() + message = "The id's don't match!! \n If you wanted to change a usergroup's id: This isn't allowed" +} \ No newline at end of file diff --git a/src/models/creation/CreateUser.ts b/src/models/creation/CreateUser.ts new file mode 100644 index 0000000..4bd8cd4 --- /dev/null +++ b/src/models/creation/CreateUser.ts @@ -0,0 +1,76 @@ +import * as argon2 from "argon2"; +import { IsEmail, IsOptional, IsPhoneNumber, IsString, IsUUID } from 'class-validator'; +import { getConnectionManager } from 'typeorm'; +import * as uuid from 'uuid'; +import { UserGroupNotFoundError, UsernameOrEmailNeededError } from '../../errors/UserErrors'; +import { User } from '../entities/User'; +import { UserGroup } from '../entities/UserGroup'; + +export class CreateUser { + @IsString() + firstname: string; + @IsString() + middlename?: string; + @IsOptional() + @IsString() + username?: string; + @IsPhoneNumber("ZZ") + @IsOptional() + phone?: string; + @IsString() + password: string; + @IsString() + lastname: string; + @IsEmail() + @IsString() + email?: string; + @IsOptional() + groupId?: number[] | number + @IsUUID("4") + uuid: string; + + public async toUser(): Promise { + let newUser: User = new User(); + + if (this.email === undefined && this.username === undefined) { + throw new UsernameOrEmailNeededError(); + } + + if (this.groupId) { + if (!Array.isArray(this.groupId)) { + this.groupId = [this.groupId] + } + const groupIDs: number[] = this.groupId + let errors = 0 + const validateusergroups = async () => { + let foundgroups = [] + for (const g of groupIDs) { + const found = await getConnectionManager().get().getRepository(UserGroup).find({ id: g }); + if (found.length === 0) { + errors++ + } else { + foundgroups.push(found[0]) + } + } + newUser.groups = foundgroups + } + await validateusergroups() + if (errors !== 0) { + throw new UserGroupNotFoundError(); + } + } + + const new_uuid = uuid.v4() + + newUser.email = this.email + newUser.username = this.username + newUser.firstname = this.firstname + newUser.middlename = this.middlename + newUser.lastname = this.lastname + newUser.uuid = new_uuid + newUser.password = await argon2.hash(this.password + new_uuid); + + console.log(newUser) + return newUser; + } +} \ No newline at end of file diff --git a/src/models/creation/CreateUserGroup.ts b/src/models/creation/CreateUserGroup.ts new file mode 100644 index 0000000..d55618c --- /dev/null +++ b/src/models/creation/CreateUserGroup.ts @@ -0,0 +1,28 @@ +import { IsOptional, IsString } from 'class-validator'; +import { GroupNameNeededError } from '../../errors/UserGroupErrors'; +import { UserGroup } from '../entities/UserGroup'; + +export class CreateUserGroup { + @IsOptional() + @IsString() + name: string; + @IsOptional() + @IsString() + description?: string; + + public async toUserGroup(): Promise { + let newUserGroup: UserGroup = new UserGroup(); + + if (this.name === undefined) { + throw new GroupNameNeededError(); + } + + newUserGroup.name = this.name + if (this.description) { + newUserGroup.description = this.description + } + + console.log(newUserGroup) + return newUserGroup; + } +} \ No newline at end of file diff --git a/src/models/entities/User.ts b/src/models/entities/User.ts index d5c4bfa..6fd3321 100644 --- a/src/models/entities/User.ts +++ b/src/models/entities/User.ts @@ -1,130 +1,128 @@ -import { Entity, Column, OneToMany, ManyToOne, PrimaryGeneratedColumn, Generated, Unique, JoinTable, ManyToMany } from "typeorm"; -import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, isUUID, } from "class-validator"; -import { UserGroup } from './UserGroup'; -import { Permission } from './Permission'; -import { UserAction } from './UserAction'; - -/** - * Defines a admin user. -*/ -@Entity() -export class User { - /** - * autogenerated unique id (primary key). - */ - @PrimaryGeneratedColumn() - @IsOptional() - @IsInt() - id: number; - - /** - * autogenerated uuid - */ - @IsOptional() - @IsInt() - @Generated("uuid") - uuid: string; - - /** - * user email - */ - @IsEmail() - email: string; - - /** - * user phone - */ - @IsPhoneNumber("ZZ") - @IsOptional() - phone: string; - - /** - * username - */ - @IsString() - username: string; - - /** - * firstname - */ - @IsString() - @IsNotEmpty() - firstname: string; - - /** - * middlename - */ - @IsString() - @IsOptional() - middlename: string; - - /** - * lastname - */ - @IsString() - @IsNotEmpty() - lastname: string; - - /** - * password - */ - @IsString() - @IsNotEmpty() - password: string; - - /** - * permissions - */ - @ManyToOne(() => Permission, permission => permission.users, { nullable: true }) - permissions: Permission[]; - - /** - * groups - */ - @ManyToMany(() => UserGroup) - @JoinTable() - groups: UserGroup[]; - - /** - * is user enabled? - */ - @IsBoolean() - enabled: boolean; - - /** - * jwt refresh count - */ - @IsInt() - @Column({ default: 1 }) - refreshTokenCount: number; - - /** - * profilepic - */ - @IsString() - profilepic: string; - - /** - * actions - */ - @OneToMany(() => UserAction, action => action.user) - actions: UserAction - - /** - * calculate all permissions - */ - public get calc_permissions(): Permission[] { - let final_permissions = [] - this.groups.forEach((permission) => { - if (!final_permissions.includes(permission)) { - final_permissions.push(permission) - } - }) - this.permissions.forEach((permission) => { - if (!final_permissions.includes(permission)) { - final_permissions.push(permission) - } - }) - return final_permissions - } -} +import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUUID } from "class-validator"; +import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { Permission } from './Permission'; +import { UserAction } from './UserAction'; +import { UserGroup } from './UserGroup'; + +/** + * Defines a admin user. +*/ +@Entity() +export class User { + /** + * autogenerated unique id (primary key). + */ + @PrimaryGeneratedColumn() + @IsOptional() + @IsInt() + id: number; + + /** + * uuid + */ + @IsUUID("4") + uuid: string; + + /** + * user email + */ + @IsEmail() + email: string; + + /** + * user phone + */ + @IsPhoneNumber("ZZ") + @IsOptional() + phone: string; + + /** + * username + */ + @IsString() + username: string; + + /** + * firstname + */ + @IsString() + @IsNotEmpty() + firstname: string; + + /** + * middlename + */ + @IsString() + @IsOptional() + middlename: string; + + /** + * lastname + */ + @IsString() + @IsNotEmpty() + lastname: string; + + /** + * password + */ + @IsString() + @IsNotEmpty() + password: string; + + /** + * permissions + */ + @ManyToOne(() => Permission, permission => permission.users, { nullable: true }) + permissions: Permission[]; + + /** + * groups + */ + @ManyToMany(() => UserGroup) + @JoinTable() + groups: UserGroup[]; + + /** + * is user enabled? + */ + @IsBoolean() + enabled: boolean; + + /** + * jwt refresh count + */ + @IsInt() + @Column({ default: 1 }) + refreshTokenCount: number; + + /** + * profilepic + */ + @IsString() + profilepic: string; + + /** + * actions + */ + @OneToMany(() => UserAction, action => action.user) + actions: UserAction + + /** + * calculate all permissions + */ + public get calc_permissions(): Permission[] { + let final_permissions = [] + this.groups.forEach((permission) => { + if (!final_permissions.includes(permission)) { + final_permissions.push(permission) + } + }) + this.permissions.forEach((permission) => { + if (!final_permissions.includes(permission)) { + final_permissions.push(permission) + } + }) + return final_permissions + } +} diff --git a/src/models/entities/UserGroup.ts b/src/models/entities/UserGroup.ts index c3b2f0c..2156d14 100644 --- a/src/models/entities/UserGroup.ts +++ b/src/models/entities/UserGroup.ts @@ -1,17 +1,17 @@ -import { PrimaryGeneratedColumn, Column, OneToMany, Entity, ManyToOne } from "typeorm"; import { IsInt, IsNotEmpty, IsOptional, - IsString, + IsString } from "class-validator"; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { Permission } from "./Permission"; /** * Defines the UserGroup interface. */ @Entity() -export abstract class UserGroup { +export class UserGroup { /** * Autogenerated unique id (primary key). */ @@ -37,7 +37,7 @@ export abstract class UserGroup { /** * The group's description */ - @Column({nullable: true}) + @Column({ nullable: true }) @IsOptional() @IsString() description?: string;