diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts
index bb8d5ee..846653f 100644
--- a/src/controllers/UserController.ts
+++ b/src/controllers/UserController.ts
@@ -1,13 +1,14 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
-import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
+import { UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/create/CreateUser';
import { UpdateUser } from '../models/actions/update/UpdateUser';
import { User } from '../models/entities/User';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUser } from '../models/responses/ResponseUser';
+import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions';
import { PermissionController } from './PermissionController';
@@ -26,7 +27,7 @@ export class UserController {
@Get()
@Authorized("USER:GET")
@ResponseSchema(ResponseUser, { isArray: true })
- @OpenAPI({ description: 'Lists all users.
This includes their groups and permissions directly granted to them (if existing/associated).' })
+ @OpenAPI({ description: 'Lists all users.
This includes their groups and permissions granted to them.' })
async getAll() {
let responseUsers: ResponseUser[] = new Array();
const users = await this.userRepository.find({ relations: ['permissions', 'groups', 'groups.permissions'] });
@@ -41,17 +42,30 @@ export class UserController {
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
- @OpenAPI({ description: 'Lists all information about the user whose id got provided.
Please remember that only permissions granted directly to the user will show up here, not permissions inherited from groups.' })
+ @OpenAPI({ description: 'Lists all information about the user whose id got provided.
Please remember that all permissions granted to the user will show up here.' })
async getOne(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
}
+ @Get('/:id/permissions')
+ @Authorized("USER:GET")
+ @ResponseSchema(ResponseUser)
+ @ResponseSchema(UserNotFoundError, { statusCode: 404 })
+ @OnUndefined(UserNotFoundError)
+ @OpenAPI({ description: 'Lists all permissions granted to the user sorted into directly granted and inherited as permission response objects.' })
+ async getPermissions(@Param('id') id: number) {
+ let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] })
+ if (!user) { throw new UserNotFoundError(); }
+ return new ResponseUserPermissions(user);
+ }
+
@Post()
@Authorized("USER:CREATE")
@ResponseSchema(ResponseUser)
- @ResponseSchema(UserGroupNotFoundError)
+ @ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
+ @ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@OpenAPI({ description: 'Create a new user.
If you want to grant permissions to the user you have to create them seperately by posting to /api/permissions after creating the user.' })
async post(@Body({ validate: true }) createUser: CreateUser) {
let user;
@@ -70,6 +84,7 @@ export class UserController {
@ResponseSchema(ResponseUser)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
+ @ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 })
@OpenAPI({ description: "Update the user whose id you provided.
To change the permissions directly granted to the user please use /api/permissions instead.
Please remember that ids can't be changed." })
async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) {
let oldUser = await this.userRepository.findOne({ id: id });
diff --git a/src/errors/UserErrors.ts b/src/errors/UserErrors.ts
index 37f2987..5d2b659 100644
--- a/src/errors/UserErrors.ts
+++ b/src/errors/UserErrors.ts
@@ -4,7 +4,7 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when no username or email is set.
- * We somehow need to identify you :)
+ * We somehow need to identify you on login.
*/
export class UsernameOrEmailNeededError extends NotFoundError {
@IsString()
@@ -14,6 +14,30 @@ export class UsernameOrEmailNeededError extends NotFoundError {
message = "No username or email is set!"
}
+/**
+ * Error to throw when no username contains illegal characters.
+ * Right now the only one is "@" but this could change in the future.
+ */
+export class UsernameContainsIllegalCharacterError extends NotAcceptableError {
+ @IsString()
+ name = "UsernameContainsIllegalCharacterError"
+
+ @IsString()
+ message = "The provided username contains illegal characters! \n Right now the following characters are considered illegal: '@'"
+}
+
+/**
+ * Error to throw when no email is set.
+ * We somehow need to identify you :)
+ */
+export class UserEmailNeededError extends NotFoundError {
+ @IsString()
+ name = "UserEmailNeededError"
+
+ @IsString()
+ message = "No email is set! \n You have to provide email addresses for users (used for password reset among others)."
+}
+
/**
* Error to throw when a user couldn't be found.
*/
diff --git a/src/models/actions/create/CreateUser.ts b/src/models/actions/create/CreateUser.ts
index a5f20c2..1942e59 100644
--- a/src/models/actions/create/CreateUser.ts
+++ b/src/models/actions/create/CreateUser.ts
@@ -1,9 +1,9 @@
import * as argon2 from "argon2";
-import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
+import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid';
import { config } from '../../../config';
-import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
+import { UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
import { User } from '../../entities/User';
import { UserGroup } from '../../entities/UserGroup';
@@ -33,7 +33,7 @@ export class CreateUser {
/**
* The new user's username.
- * You have to provide at least one of: {email, username}.
+ * You have to provide a email addres, so this is optional.
*/
@IsOptional()
@IsString()
@@ -41,12 +41,11 @@ export class CreateUser {
/**
* The new user's email address.
- * You have to provide at least one of: {email, username}.
*/
@IsEmail()
@IsString()
- @IsOptional()
- email?: string;
+ @IsNotEmpty()
+ email: string;
/**
* The new user's phone number.
@@ -92,9 +91,10 @@ export class CreateUser {
public async toEntity(): Promise {
let newUser: User = new User();
- if (this.email === undefined && this.username === undefined) {
- throw new UsernameOrEmailNeededError();
+ if (!this.email) {
+ throw new UserEmailNeededError();
}
+ if (this.username.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
newUser.email = this.email
newUser.username = this.username
diff --git a/src/models/actions/update/UpdateUser.ts b/src/models/actions/update/UpdateUser.ts
index 59d95a4..2797b34 100644
--- a/src/models/actions/update/UpdateUser.ts
+++ b/src/models/actions/update/UpdateUser.ts
@@ -1,8 +1,8 @@
import * as argon2 from "argon2";
-import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
+import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../../config';
-import { UsernameOrEmailNeededError } from '../../../errors/AuthError';
+import { UserEmailNeededError, UsernameContainsIllegalCharacterError } from '../../../errors/UserErrors';
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
import { User } from '../../entities/User';
import { UserGroup } from '../../entities/UserGroup';
@@ -40,7 +40,7 @@ export class UpdateUser {
/**
* The updated user's username.
- * You have to provide at least one of: {email, username}.
+ * You have to provide a email addres, so this is optional.
*/
@IsOptional()
@IsString()
@@ -48,12 +48,11 @@ export class UpdateUser {
/**
* The updated user's email address.
- * You have to provide at least one of: {email, username}.
*/
@IsEmail()
@IsString()
- @IsOptional()
- email?: string;
+ @IsNotEmpty()
+ email: string;
/**
* The updated user's phone number.
@@ -99,16 +98,18 @@ export class UpdateUser {
* @param user The user that shall be updated.
*/
public async update(user: User): Promise {
- user.email = this.email;
- user.username = this.username;
- if ((user.email === undefined || user.email === null) && (user.username === undefined || user.username === null)) {
- throw new UsernameOrEmailNeededError();
+ if (!this.email) {
+ throw new UserEmailNeededError();
}
+ if (this.username.includes("@")) { throw new UsernameContainsIllegalCharacterError(); }
+
if (this.password) {
user.password = await argon2.hash(this.password + user.uuid);
user.refreshTokenCount = user.refreshTokenCount + 1;
}
+ user.email = this.email;
+ user.username = this.username;
user.enabled = this.enabled;
user.firstname = this.firstname
user.middlename = this.middlename
diff --git a/src/models/entities/User.ts b/src/models/entities/User.ts
index 94e091c..c9111ff 100644
--- a/src/models/entities/User.ts
+++ b/src/models/entities/User.ts
@@ -3,6 +3,7 @@ import { ChildEntity, Column, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { ResponseUser } from '../responses/ResponseUser';
+import { Permission } from './Permission';
import { Principal } from './Principal';
import { UserAction } from './UserAction';
import { UserGroup } from './UserGroup';
@@ -25,9 +26,10 @@ export class User extends Principal {
* The user's e-mail address.
* Either username or email has to be set (otherwise the user couldn't log in).
*/
- @Column({ nullable: true, unique: true })
+ @Column({ nullable: false, unique: true })
@IsEmail()
- email?: string;
+ @IsNotEmpty()
+ email: string;
/**
* The user's phone number.
@@ -128,8 +130,24 @@ export class User extends Principal {
@OneToMany(() => UserAction, action => action.user, { nullable: true })
actions: UserAction[]
+ /**
+ * Resolves all permissions granted to this user through groups.
+ */
+ public get inheritedPermissions(): Permission[] {
+ let returnPermissions: Permission[] = new Array();
+
+ if (!this.groups) { return returnPermissions; }
+ for (let group of this.groups) {
+ for (let permission of group.permissions) {
+ returnPermissions.push(permission);
+ }
+ }
+ return returnPermissions;
+ }
+
/**
* Resolves all permissions granted to this user through groups or directly to the string enum format.
+ * Also deduplicates the array.
*/
public get allPermissions(): string[] {
let returnPermissions: string[] = new Array();
diff --git a/src/models/responses/ResponseUser.ts b/src/models/responses/ResponseUser.ts
index 3da5434..526d537 100644
--- a/src/models/responses/ResponseUser.ts
+++ b/src/models/responses/ResponseUser.ts
@@ -70,6 +70,7 @@ export class ResponseUser extends ResponsePrincipal {
/**
* The user's permissions.
+ * Directly granted or inherited converted to their string form and deduplicated.
*/
@IsArray()
@IsOptional()
diff --git a/src/models/responses/ResponseUserPermissions.ts b/src/models/responses/ResponseUserPermissions.ts
new file mode 100644
index 0000000..d5a8a7b
--- /dev/null
+++ b/src/models/responses/ResponseUserPermissions.ts
@@ -0,0 +1,40 @@
+import {
+ IsArray,
+
+
+ IsOptional
+} from "class-validator";
+import { User } from '../entities/User';
+import { ResponsePermission } from './ResponsePermission';
+
+/**
+ * Defines the user permission response (get /api/users/:id/permissions).
+*/
+export class ResponseUserPermissions {
+ /**
+ * The permissions directly granted to the user.
+ */
+ @IsArray()
+ @IsOptional()
+ directlyGranted: ResponsePermission[] = new Array();
+
+ /**
+ * The permissions directly inherited the user.
+ */
+ @IsArray()
+ @IsOptional()
+ inherited: ResponsePermission[] = new Array();
+
+ /**
+ * Creates a ResponseUserPermissions object from a user.
+ * @param user The user the response shall be build for.
+ */
+ public constructor(user: User) {
+ for (let permission of user.permissions) {
+ this.directlyGranted.push(permission.toResponse());
+ }
+ for (let permission of user.inheritedPermissions) {
+ this.inherited.push(permission.toResponse());
+ }
+ }
+}
diff --git a/src/seeds/SeedUsers.ts b/src/seeds/SeedUsers.ts
index e70ef84..26fc233 100644
--- a/src/seeds/SeedUsers.ts
+++ b/src/seeds/SeedUsers.ts
@@ -33,6 +33,7 @@ export default class SeedUsers implements Seeder {
initialUser.lastname = "demo";
initialUser.username = "demo";
initialUser.password = "demo";
+ initialUser.email = "demo@dev.lauf-fuer-kaya.de"
initialUser.groups = group;
return await connection.getRepository(User).save(await initialUser.toEntity());
}
diff --git a/src/tests/auth/auth_logout.spec.ts b/src/tests/auth/auth_logout.spec.ts
index 7c846c1..2594f26 100644
--- a/src/tests/auth/auth_logout.spec.ts
+++ b/src/tests/auth/auth_logout.spec.ts
@@ -14,7 +14,8 @@ beforeAll(async () => {
"middlename": "demo_logout",
"lastname": "demo_logout",
"username": "demo_logout",
- "password": "demo_logout"
+ "password": "demo_logout",
+ "email": "demo_logout@dev.lauf-fuer-kaya.de"
}, {
headers: { "authorization": "Bearer " + res_login.data["access_token"] },
validateStatus: undefined
diff --git a/src/tests/auth/auth_refresh.spec.ts b/src/tests/auth/auth_refresh.spec.ts
index 02d2a02..bd875ba 100644
--- a/src/tests/auth/auth_refresh.spec.ts
+++ b/src/tests/auth/auth_refresh.spec.ts
@@ -14,7 +14,8 @@ beforeAll(async () => {
"middlename": "demo_refresh",
"lastname": "demo_refresh",
"username": "demo_refresh",
- "password": "demo_refresh"
+ "password": "demo_refresh",
+ "email": "demo_refresh@dev.lauf-fuer-kaya.de"
}, {
headers: { "authorization": "Bearer " + res_login.data["access_token"] },
validateStatus: undefined
diff --git a/src/tests/auth/auth_reset.spec.ts b/src/tests/auth/auth_reset.spec.ts
index 2ebb8ed..47c61e6 100644
--- a/src/tests/auth/auth_reset.spec.ts
+++ b/src/tests/auth/auth_reset.spec.ts
@@ -14,7 +14,8 @@ beforeAll(async () => {
"middlename": "demo_reset",
"lastname": "demo_reset",
"username": "demo_reset",
- "password": "demo_reset"
+ "password": "demo_reset",
+ "email": "demo_reset@dev.lauf-fuer-kaya.de"
}, {
headers: { "authorization": "Bearer " + res_login.data["access_token"] },
validateStatus: undefined
@@ -24,7 +25,8 @@ beforeAll(async () => {
"middlename": "demo_reset2",
"lastname": "demo_reset2",
"username": "demo_reset2",
- "password": "demo_reset2"
+ "password": "demo_reset2",
+ "email": "demo_reset1@dev.lauf-fuer-kaya.de"
}, {
headers: { "authorization": "Bearer " + res_login.data["access_token"] },
validateStatus: undefined