From e586a11e2ad42af9c9bb5d2a47f48e3306fe49b2 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 21:57:39 +0100 Subject: [PATCH 1/8] Created barebones file for the userchecker ref #100 --- src/middlewares/UserChecker.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/middlewares/UserChecker.ts diff --git a/src/middlewares/UserChecker.ts b/src/middlewares/UserChecker.ts new file mode 100644 index 0000000..93408ef --- /dev/null +++ b/src/middlewares/UserChecker.ts @@ -0,0 +1,9 @@ +import { Action } from 'routing-controllers'; + +/** + * TODO: + */ +const UserChecker = async (action: Action) => { + +}; +export default UserChecker; \ No newline at end of file From f1db8836092269966a7f54e69b1f20c171e81b21 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:16:28 +0100 Subject: [PATCH 2/8] Implemented a baisc user checker/getter ref #100 --- src/app.ts | 2 ++ src/middlewares/UserChecker.ts | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/app.ts b/src/app.ts index 44060da..6e814b3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,10 +5,12 @@ import { config, e as errors } from './config'; import loaders from "./loaders/index"; import authchecker from "./middlewares/authchecker"; import { ErrorHandler } from './middlewares/ErrorHandler'; +import UserChecker from './middlewares/UserChecker'; const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts'; const app = createExpressServer({ authorizationChecker: authchecker, + currentUserChecker: UserChecker, middlewares: [ErrorHandler], development: config.development, cors: true, diff --git a/src/middlewares/UserChecker.ts b/src/middlewares/UserChecker.ts index 93408ef..603fe6e 100644 --- a/src/middlewares/UserChecker.ts +++ b/src/middlewares/UserChecker.ts @@ -1,9 +1,58 @@ +import cookie from "cookie"; +import * as jwt from "jsonwebtoken"; import { Action } from 'routing-controllers'; +import { getConnectionManager } from 'typeorm'; +import { config } from '../config'; +import { IllegalJWTError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError'; +import { JwtCreator, JwtUser } from '../jwtcreator'; +import { User } from '../models/entities/User'; /** * TODO: */ const UserChecker = async (action: Action) => { + let jwtPayload = undefined + try { + let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", ""); + jwtPayload = jwt.verify(provided_token, config.jwt_secret); + jwtPayload = jwtPayload["userdetails"]; + } catch (error) { + jwtPayload = await refresh(action); + } + const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }) + if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() } + if (user.enabled == false) { throw new UserDisabledError(); } + return user; }; + +/** + * Handles soft-refreshing of access-tokens. + * @param action Routing-Controllers action object that provides request and response objects among other stuff. + */ +const refresh = async (action: Action) => { + let refresh_token = undefined; + try { + refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"]; + } + catch { + throw new IllegalJWTError(); + } + + let jwtPayload = undefined; + try { + jwtPayload = jwt.verify(refresh_token, config.jwt_secret); + } catch (error) { + throw new IllegalJWTError(); + } + + const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] }) + if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() } + if (user.enabled == false) { throw new UserDisabledError(); } + + let newAccess = JwtCreator.createAccess(user); + action.response.header("authorization", "Bearer " + newAccess); + + return await new JwtUser(user); +} export default UserChecker; \ No newline at end of file From a334adffc6d07c8ab340263123e00a96f21acecb Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:27:44 +0100 Subject: [PATCH 3/8] Moved optional param to being optional ref #100 --- src/models/actions/update/UpdateUser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/actions/update/UpdateUser.ts b/src/models/actions/update/UpdateUser.ts index c9dedcf..f130672 100644 --- a/src/models/actions/update/UpdateUser.ts +++ b/src/models/actions/update/UpdateUser.ts @@ -76,6 +76,7 @@ export class UpdateUser { * Should the user be enabled? */ @IsBoolean() + @IsOptional() enabled: boolean = true; /** From 8ef5f90abda97a73d5c5a7767a144ac3fb5288c1 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:28:18 +0100 Subject: [PATCH 4/8] Implemented the /me controller that allows a user to get and update themselves ref #100 --- src/controllers/MeController.ts | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/controllers/MeController.ts diff --git a/src/controllers/MeController.ts b/src/controllers/MeController.ts new file mode 100644 index 0000000..8895073 --- /dev/null +++ b/src/controllers/MeController.ts @@ -0,0 +1,54 @@ +import { Body, CurrentUser, Get, JsonController, OnUndefined, Put } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors'; +import { UpdateUser } from '../models/actions/update/UpdateUser'; +import { User } from '../models/entities/User'; +import { ResponseUser } from '../models/responses/ResponseUser'; + + +@JsonController('/me') +@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) +export class MeController { + private userRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.userRepository = getConnectionManager().get().getRepository(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 get(@CurrentUser() currentUser: User) { + let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] }) + if (!user) { throw new UserNotFoundError(); } + return new ResponseUser(user); + } + + @Put('/') + @ResponseSchema(ResponseUser) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) + @ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 }) + @ResponseSchema(UsernameContainsIllegalCharacterError, { statusCode: 406 }) + @OpenAPI({ description: "Update the yourself.
You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead.
Please remember that ids can't be changed." }) + async put(@CurrentUser() currentUser: User, @Body({ validate: true }) updateUser: UpdateUser) { + let oldUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['groups'] }); + updateUser.groups = oldUser.groups.map(g => g.id); + + if (!oldUser) { + throw new UserNotFoundError(); + } + + if (oldUser.id != updateUser.id) { + throw new UserIdsNotMatchingError(); + } + await this.userRepository.save(await updateUser.update(oldUser)); + + return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] })); + } +} From 6b7ecd3044c45b2eed46ee5010bed4dab4f02df9 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:35:23 +0100 Subject: [PATCH 5/8] User deletion now requires confirmation ref #100 --- src/controllers/UserController.ts | 6 ++++-- src/errors/UserErrors.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 846653f..0c5f0cb 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -1,7 +1,7 @@ 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, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors'; +import { UserDeletionNotConfirmedError, 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'; @@ -105,9 +105,11 @@ export class UserController { @Authorized("USER:DELETE") @ResponseSchema(ResponseUser) @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 }) @OnUndefined(204) - @OpenAPI({ description: 'Delete the user whose id you provided.
If there are any permissions directly granted to the user they will get deleted as well.
If no user with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete the user whose id you provided.
You have to confirm your decision by providing the ?force=true query param.
If there are any permissions directly granted to the user they will get deleted as well.
If no user with this id exists it will just return 204(no content).' }) async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + if (!force) { throw new UserDeletionNotConfirmedError; } let user = await this.userRepository.findOne({ id: id }); if (!user) { return null; } const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });; diff --git a/src/errors/UserErrors.ts b/src/errors/UserErrors.ts index 5d2b659..ced02ed 100644 --- a/src/errors/UserErrors.ts +++ b/src/errors/UserErrors.ts @@ -59,4 +59,16 @@ export class UserIdsNotMatchingError extends NotAcceptableError { @IsString() message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!" +} + +/** + * Error to throw when two users' ids don't match. + * Usually occurs when a user tries to change a user's id. + */ +export class UserDeletionNotConfirmedError extends NotAcceptableError { + @IsString() + name = "UserDeletionNotConfirmedError" + + @IsString() + message = "You are trying to delete a user! \n If you're sure about doing this: provide the ?force=true query param." } \ No newline at end of file From 4f6e81677c81c852e735407295c634b43b317479 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:35:50 +0100 Subject: [PATCH 6/8] Implemented getting own permissions ref #100 --- src/controllers/MeController.ts | 42 +++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/controllers/MeController.ts b/src/controllers/MeController.ts index 8895073..bcdb48b 100644 --- a/src/controllers/MeController.ts +++ b/src/controllers/MeController.ts @@ -1,10 +1,13 @@ -import { Body, CurrentUser, Get, JsonController, OnUndefined, Put } from 'routing-controllers'; +import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Put, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors'; +import { UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors'; 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'; @JsonController('/me') @@ -23,13 +26,24 @@ export class MeController { @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.' }) + @OpenAPI({ description: 'Lists all information about yourself.' }) async get(@CurrentUser() currentUser: User) { let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] }) if (!user) { throw new UserNotFoundError(); } return new ResponseUser(user); } + @Get('/') + @ResponseSchema(ResponseUserPermissions) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) + @OnUndefined(UserNotFoundError) + @OpenAPI({ description: 'Lists all permissions granted to the you sorted into directly granted and inherited as permission response objects.' }) + async getPermissions(@CurrentUser() currentUser: User) { + let user = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions', 'permissions.principal', 'groups.permissions.principal'] }) + if (!user) { throw new UserNotFoundError(); } + return new ResponseUserPermissions(user); + } + @Put('/') @ResponseSchema(ResponseUser) @ResponseSchema(UserNotFoundError, { statusCode: 404 }) @@ -51,4 +65,24 @@ export class MeController { return new ResponseUser(await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] })); } -} + + @Delete('/') + @ResponseSchema(ResponseUser) + @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 }) + @OnUndefined(204) + @OpenAPI({ description: 'Delete the user whose id you provided.
If there are any permissions directly granted to the user they will get deleted as well.
If no user with this id exists it will just return 204(no content).' }) + async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) { + if (!force) { throw new UserDeletionNotConfirmedError; } + if (!currentUser) { return null; } + const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });; + + const permissionControler = new PermissionController(); + for (let permission of responseUser.permissions) { + await permissionControler.remove(permission.id, true); + } + + await this.userRepository.delete(currentUser); + return new ResponseUser(responseUser); + } +} \ No newline at end of file From fc7b8f4c16cef0e72b04f096d5a17d4144b5feb7 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:43:22 +0100 Subject: [PATCH 7/8] Updated descriptions and responses ref #100 --- src/controllers/MeController.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/controllers/MeController.ts b/src/controllers/MeController.ts index bcdb48b..a9ff4ac 100644 --- a/src/controllers/MeController.ts +++ b/src/controllers/MeController.ts @@ -4,7 +4,6 @@ import { getConnectionManager, Repository } from 'typeorm'; import { UserDeletionNotConfirmedError, UserIdsNotMatchingError, UsernameContainsIllegalCharacterError, UserNotFoundError } from '../errors/UserErrors'; 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'; @@ -68,13 +67,12 @@ export class MeController { @Delete('/') @ResponseSchema(ResponseUser) - @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) @ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 }) - @OnUndefined(204) - @OpenAPI({ description: 'Delete the user whose id you provided.
If there are any permissions directly granted to the user they will get deleted as well.
If no user with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete yourself.
You have to confirm your decision by providing the ?force=true query param.
If there are any permissions directly granted to you they will get deleted as well.' }) async remove(@CurrentUser() currentUser: User, @QueryParam("force") force: boolean) { if (!force) { throw new UserDeletionNotConfirmedError; } - if (!currentUser) { return null; } + if (!currentUser) { return UserNotFoundError; } const responseUser = await this.userRepository.findOne({ id: currentUser.id }, { relations: ['permissions', 'groups', 'groups.permissions'] });; const permissionControler = new PermissionController(); From f9834b5f4d80b11ee5f7773b339dd421341c6e7f Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 15 Jan 2021 22:45:45 +0100 Subject: [PATCH 8/8] Moved the me endpoints to /users/me ref #100 --- src/controllers/MeController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/MeController.ts b/src/controllers/MeController.ts index a9ff4ac..08fdc51 100644 --- a/src/controllers/MeController.ts +++ b/src/controllers/MeController.ts @@ -9,7 +9,7 @@ import { ResponseUserPermissions } from '../models/responses/ResponseUserPermiss import { PermissionController } from './PermissionController'; -@JsonController('/me') +@JsonController('/users/me') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) export class MeController { private userRepository: Repository;