feat(web,server): manage authorized devices (#2329)

* feat: manage authorized devices

* chore: open api

* get header from mobile app

* write header from mobile app

* styling

* fix unit test

* feat: use relative time

* feat: update access time

* fix: tests

* chore: confirm wording

* chore: bump test coverage thresholds

* feat: add some icons

* chore: icon tweaks

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-04-25 22:19:23 -04:00
committed by GitHub
parent aa91b946fa
commit b8313abfa8
41 changed files with 1209 additions and 93 deletions

View File

@@ -1,5 +1,6 @@
import {
AdminSignupResponseDto,
AuthDeviceResponseDto,
AuthService,
AuthType,
AuthUserDto,
@@ -7,18 +8,20 @@ import {
IMMICH_ACCESS_COOKIE,
IMMICH_AUTH_TYPE_COOKIE,
LoginCredentialDto,
LoginDetails,
LoginResponseDto,
LogoutResponseDto,
SignUpDto,
UserResponseDto,
ValidateAccessTokenResponseDto,
} from '@app/domain';
import { Body, Controller, Ip, Post, Req, Res } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common';
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { GetAuthUser } from '../decorators/auth-user.decorator';
import { GetAuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
import { UseValidation } from '../decorators/use-validation.decorator';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('Authentication')
@Controller('auth')
@@ -29,11 +32,10 @@ export class AuthController {
@Post('login')
async login(
@Body() loginCredential: LoginCredentialDto,
@Ip() clientIp: string,
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
@GetLoginDetails() loginDetails: LoginDetails,
): Promise<LoginResponseDto> {
const { response, cookie } = await this.service.login(loginCredential, clientIp, req.secure);
const { response, cookie } = await this.service.login(loginCredential, loginDetails);
res.header('Set-Cookie', cookie);
return response;
}
@@ -44,6 +46,18 @@ export class AuthController {
return this.service.adminSignUp(signUpCredential);
}
@Authenticated()
@Get('devices')
getAuthDevices(@GetAuthUser() authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
return this.service.getDevices(authUser);
}
@Authenticated()
@Delete('devices/:id')
logoutAuthDevice(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.logoutDevice(authUser, id);
}
@Authenticated()
@Post('validateToken')
validateAccessToken(): ValidateAccessTokenResponseDto {

View File

@@ -1,5 +1,6 @@
import {
AuthUserDto,
LoginDetails,
LoginResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
@@ -10,7 +11,7 @@ import {
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { GetAuthUser } from '../decorators/auth-user.decorator';
import { GetAuthUser, GetLoginDetails } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator';
import { UseValidation } from '../decorators/use-validation.decorator';
@@ -38,9 +39,9 @@ export class OAuthController {
async callback(
@Res({ passthrough: true }) res: Response,
@Body() dto: OAuthCallbackDto,
@Req() req: Request,
@GetLoginDetails() loginDetails: LoginDetails,
): Promise<LoginResponseDto> {
const { response, cookie } = await this.service.login(dto, req.secure);
const { response, cookie } = await this.service.login(dto, loginDetails);
res.header('Set-Cookie', cookie);
return response;
}

View File

@@ -1,7 +1,20 @@
export { AuthUserDto } from '@app/domain';
import { AuthUserDto } from '@app/domain';
import { AuthUserDto, LoginDetails } from '@app/domain';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { UAParser } from 'ua-parser-js';
export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user;
});
export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
const req = ctx.switchToHttp().getRequest();
const userAgent = UAParser(req.headers['user-agent']);
return {
clientIp: req.clientIp,
isSecure: req.secure,
deviceType: userAgent.browser.name || userAgent.device.type || req.headers.devicemodel || '',
deviceOS: userAgent.os.name || req.headers.devicetype || '',
};
});

View File

@@ -21,6 +21,10 @@ export function patchOpenAPI(document: OpenAPIObject) {
if (operation.summary === '') {
delete operation.summary;
}
if (operation.description === '') {
delete operation.description;
}
}
}