Merge pull request 'User self-management feature/100-me_endpoints' (#103) from feature/100-me_endpoints into dev
continuous-integration/drone/push Build is passing Details

Reviewed-on: #103
This commit is contained in:
Nicolai Ort 2021-01-15 21:57:21 +00:00
commit a6c7d54fe7
6 changed files with 163 additions and 2 deletions

View File

@ -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,

View File

@ -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<User>;
/**
* 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. <br> You can't edit your own permissions or group memberships here - Please use the /api/users/:id enpoint instead. <br> 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. <br> You have to confirm your decision by providing the ?force=true query param. <br> 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);
}
}

View File

@ -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. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> If no user with this id exists it will just return 204(no content).' })
@OpenAPI({ description: 'Delete the user whose id you provided. <br> You have to confirm your decision by providing the ?force=true query param. <br> If there are any permissions directly granted to the user they will get deleted as well. <br> 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'] });;

View File

@ -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."
}

View File

@ -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 = <any>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 = <any>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;

View File

@ -76,6 +76,7 @@ export class UpdateUser {
* Should the user be enabled?
*/
@IsBoolean()
@IsOptional()
enabled: boolean = true;
/**