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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1209 additions and 93 deletions

View file

@ -12,7 +12,7 @@ import { OAuthCore } from '../oauth/oauth.core';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore } from '../user';
import { AuthType, IMMICH_ACCESS_COOKIE } from './auth.constant';
import { AuthCore } from './auth.core';
import { AuthCore, LoginDetails } from './auth.core';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto';
import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
@ -21,6 +21,7 @@ import cookieParser from 'cookie';
import { ISharedLinkRepository, ShareCore } from '../share';
import { APIKeyCore } from '../api-key/api-key.core';
import { IKeyRepository } from '../api-key';
import { AuthDeviceResponseDto, mapUserToken } from './response-dto';
@Injectable()
export class AuthService {
@ -53,8 +54,7 @@ export class AuthService {
public async login(
loginCredential: LoginCredentialDto,
clientIp: string,
isSecure: boolean,
loginDetails: LoginDetails,
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
if (!this.authCore.isPasswordLoginEnabled()) {
throw new UnauthorizedException('Password login has been disabled');
@ -69,16 +69,18 @@ export class AuthService {
}
if (!user) {
this.logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`);
this.logger.warn(
`Failed login attempt for user ${loginCredential.email} from ip address ${loginDetails.clientIp}`,
);
throw new BadRequestException('Incorrect email or password');
}
return this.authCore.createLoginResponse(user, AuthType.PASSWORD, isSecure);
return this.authCore.createLoginResponse(user, AuthType.PASSWORD, loginDetails);
}
public async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> {
if (authUser.accessTokenId) {
await this.userTokenCore.deleteToken(authUser.accessTokenId);
await this.userTokenCore.delete(authUser.id, authUser.accessTokenId);
}
if (authType === AuthType.OAUTH) {
@ -152,6 +154,15 @@ export class AuthService {
throw new UnauthorizedException('Authentication required');
}
async getDevices(authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
const userTokens = await this.userTokenCore.getAll(authUser.id);
return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId));
}
async logoutDevice(authUser: AuthUserDto, deviceId: string): Promise<void> {
await this.userTokenCore.delete(authUser.id, deviceId);
}
private getBearerToken(headers: IncomingHttpHeaders): string | null {
const [type, token] = (headers.authorization || '').split(' ');
if (type.toLowerCase() === 'bearer') {