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/controllers/MeController.ts b/src/controllers/MeController.ts new file mode 100644 index 0000000..08fdc51 --- /dev/null +++ b/src/controllers/MeController.ts @@ -0,0 +1,86 @@ +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 { UserDeletionNotConfirmedError, 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'; +import { ResponseUserPermissions } from '../models/responses/ResponseUserPermissions'; +import { PermissionController } from './PermissionController'; + + +@JsonController('/users/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 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 }) + @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'] })); + } + + @Delete('/') + @ResponseSchema(ResponseUser) + @ResponseSchema(UserNotFoundError, { statusCode: 404 }) + @ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 }) + @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 UserNotFoundError; } + 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 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 diff --git a/src/middlewares/UserChecker.ts b/src/middlewares/UserChecker.ts new file mode 100644 index 0000000..603fe6e --- /dev/null +++ b/src/middlewares/UserChecker.ts @@ -0,0 +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 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; /**