Merge pull request 'User self-management feature/100-me_endpoints' (#103) from feature/100-me_endpoints into dev
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #103
This commit is contained in:
commit
a6c7d54fe7
@ -5,10 +5,12 @@ import { config, e as errors } from './config';
|
|||||||
import loaders from "./loaders/index";
|
import loaders from "./loaders/index";
|
||||||
import authchecker from "./middlewares/authchecker";
|
import authchecker from "./middlewares/authchecker";
|
||||||
import { ErrorHandler } from './middlewares/ErrorHandler';
|
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||||
|
import UserChecker from './middlewares/UserChecker';
|
||||||
|
|
||||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||||
const app = createExpressServer({
|
const app = createExpressServer({
|
||||||
authorizationChecker: authchecker,
|
authorizationChecker: authchecker,
|
||||||
|
currentUserChecker: UserChecker,
|
||||||
middlewares: [ErrorHandler],
|
middlewares: [ErrorHandler],
|
||||||
development: config.development,
|
development: config.development,
|
||||||
cors: true,
|
cors: true,
|
||||||
|
86
src/controllers/MeController.ts
Normal file
86
src/controllers/MeController.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
|
||||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||||
import { getConnectionManager, Repository } from 'typeorm';
|
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 { UserGroupNotFoundError } from '../errors/UserGroupErrors';
|
||||||
import { CreateUser } from '../models/actions/create/CreateUser';
|
import { CreateUser } from '../models/actions/create/CreateUser';
|
||||||
import { UpdateUser } from '../models/actions/update/UpdateUser';
|
import { UpdateUser } from '../models/actions/update/UpdateUser';
|
||||||
@ -105,9 +105,11 @@ export class UserController {
|
|||||||
@Authorized("USER:DELETE")
|
@Authorized("USER:DELETE")
|
||||||
@ResponseSchema(ResponseUser)
|
@ResponseSchema(ResponseUser)
|
||||||
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
|
||||||
|
@ResponseSchema(UserDeletionNotConfirmedError, { statusCode: 406 })
|
||||||
@OnUndefined(204)
|
@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) {
|
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
|
||||||
|
if (!force) { throw new UserDeletionNotConfirmedError; }
|
||||||
let user = await this.userRepository.findOne({ id: id });
|
let user = await this.userRepository.findOne({ id: id });
|
||||||
if (!user) { return null; }
|
if (!user) { return null; }
|
||||||
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
|
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups', 'groups.permissions'] });;
|
||||||
|
@ -59,4 +59,16 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
|
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."
|
||||||
}
|
}
|
58
src/middlewares/UserChecker.ts
Normal file
58
src/middlewares/UserChecker.ts
Normal 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;
|
@ -76,6 +76,7 @@ export class UpdateUser {
|
|||||||
* Should the user be enabled?
|
* Should the user be enabled?
|
||||||
*/
|
*/
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user