2023-07-15 06:03:56 +02:00
|
|
|
import { SystemConfig, UserEntity } from '@app/infra/entities';
|
2023-09-01 13:08:42 +02:00
|
|
|
import {
|
|
|
|
BadRequestException,
|
|
|
|
Inject,
|
|
|
|
Injectable,
|
|
|
|
InternalServerErrorException,
|
|
|
|
Logger,
|
|
|
|
UnauthorizedException,
|
|
|
|
} from '@nestjs/common';
|
2023-06-16 21:54:17 +02:00
|
|
|
import cookieParser from 'cookie';
|
2023-01-24 05:13:42 +01:00
|
|
|
import { IncomingHttpHeaders } from 'http';
|
2023-07-15 06:03:56 +02:00
|
|
|
import { DateTime } from 'luxon';
|
2023-09-04 21:45:59 +02:00
|
|
|
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
2023-06-16 21:54:17 +02:00
|
|
|
import { IKeyRepository } from '../api-key';
|
|
|
|
import { ICryptoRepository } from '../crypto/crypto.repository';
|
2023-06-21 03:08:43 +02:00
|
|
|
import { ISharedLinkRepository } from '../shared-link';
|
2023-07-15 06:03:56 +02:00
|
|
|
import { ISystemConfigRepository } from '../system-config';
|
|
|
|
import { SystemConfigCore } from '../system-config/system-config.core';
|
|
|
|
import { IUserRepository, UserCore, UserResponseDto } from '../user';
|
|
|
|
import {
|
|
|
|
AuthType,
|
|
|
|
IMMICH_ACCESS_COOKIE,
|
|
|
|
IMMICH_API_KEY_HEADER,
|
|
|
|
IMMICH_AUTH_TYPE_COOKIE,
|
|
|
|
LOGIN_URL,
|
|
|
|
MOBILE_REDIRECT,
|
|
|
|
} from './auth.constant';
|
|
|
|
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, OAuthCallbackDto, OAuthConfigDto, SignUpDto } from './dto';
|
2023-06-16 21:54:17 +02:00
|
|
|
import {
|
|
|
|
AdminSignupResponseDto,
|
|
|
|
AuthDeviceResponseDto,
|
|
|
|
LoginResponseDto,
|
|
|
|
LogoutResponseDto,
|
2023-09-04 21:45:59 +02:00
|
|
|
OAuthAuthorizeResponseDto,
|
|
|
|
OAuthConfigResponseDto,
|
2023-06-16 21:54:17 +02:00
|
|
|
mapAdminSignupResponse,
|
2023-07-15 06:03:56 +02:00
|
|
|
mapLoginResponse,
|
2023-06-16 21:54:17 +02:00
|
|
|
mapUserToken,
|
|
|
|
} from './response-dto';
|
2023-07-15 06:03:56 +02:00
|
|
|
import { IUserTokenRepository } from './user-token.repository';
|
|
|
|
|
|
|
|
export interface LoginDetails {
|
|
|
|
isSecure: boolean;
|
|
|
|
clientIp: string;
|
|
|
|
deviceType: string;
|
|
|
|
deviceOS: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface LoginResponse {
|
|
|
|
response: LoginResponseDto;
|
|
|
|
cookie: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
interface OAuthProfile extends UserinfoResponse {
|
|
|
|
email: string;
|
|
|
|
}
|
2023-01-24 05:13:42 +01:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class AuthService {
|
|
|
|
private userCore: UserCore;
|
2023-07-15 06:03:56 +02:00
|
|
|
private configCore: SystemConfigCore;
|
2023-01-24 05:13:42 +01:00
|
|
|
private logger = new Logger(AuthService.name);
|
|
|
|
|
|
|
|
constructor(
|
2023-07-01 03:49:30 +02:00
|
|
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
2023-01-24 05:13:42 +01:00
|
|
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
|
|
|
@Inject(IUserRepository) userRepository: IUserRepository,
|
2023-07-15 06:03:56 +02:00
|
|
|
@Inject(IUserTokenRepository) private userTokenRepository: IUserTokenRepository,
|
2023-06-21 03:08:43 +02:00
|
|
|
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
2023-07-01 03:49:30 +02:00
|
|
|
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
2023-01-24 05:13:42 +01:00
|
|
|
) {
|
2023-07-15 06:03:56 +02:00
|
|
|
this.configCore = new SystemConfigCore(configRepository);
|
2023-01-27 21:50:07 +01:00
|
|
|
this.userCore = new UserCore(userRepository, cryptoRepository);
|
2023-07-15 06:03:56 +02:00
|
|
|
|
|
|
|
custom.setHttpOptionsDefaults({ timeout: 30000 });
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
async login(dto: LoginCredentialDto, details: LoginDetails): Promise<LoginResponse> {
|
|
|
|
const config = await this.configCore.getConfig();
|
|
|
|
if (!config.passwordLogin.enabled) {
|
2023-01-24 05:13:42 +01:00
|
|
|
throw new UnauthorizedException('Password login has been disabled');
|
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
let user = await this.userCore.getByEmail(dto.email, true);
|
2023-01-24 05:13:42 +01:00
|
|
|
if (user) {
|
2023-07-15 06:03:56 +02:00
|
|
|
const isAuthenticated = this.validatePassword(dto.password, user);
|
2023-01-24 05:13:42 +01:00
|
|
|
if (!isAuthenticated) {
|
|
|
|
user = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!user) {
|
2023-07-15 06:03:56 +02:00
|
|
|
this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`);
|
2023-08-01 17:49:50 +02:00
|
|
|
throw new UnauthorizedException('Incorrect email or password');
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
return this.createLoginResponse(user, AuthType.PASSWORD, details);
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> {
|
2023-02-06 06:31:16 +01:00
|
|
|
if (authUser.accessTokenId) {
|
2023-07-15 06:03:56 +02:00
|
|
|
await this.userTokenRepository.delete(authUser.id, authUser.accessTokenId);
|
2023-02-06 06:31:16 +01:00
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
return {
|
|
|
|
successful: true,
|
|
|
|
redirectUri: await this.getLogoutEndpoint(authType),
|
|
|
|
};
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) {
|
2023-01-24 05:13:42 +01:00
|
|
|
const { password, newPassword } = dto;
|
|
|
|
const user = await this.userCore.getByEmail(authUser.email, true);
|
|
|
|
if (!user) {
|
|
|
|
throw new UnauthorizedException();
|
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
const valid = this.validatePassword(password, user);
|
2023-01-24 05:13:42 +01:00
|
|
|
if (!valid) {
|
|
|
|
throw new BadRequestException('Wrong password');
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.userCore.updateUser(authUser, authUser.id, { password: newPassword });
|
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
async adminSignUp(dto: SignUpDto): Promise<AdminSignupResponseDto> {
|
2023-01-24 05:13:42 +01:00
|
|
|
const adminUser = await this.userCore.getAdmin();
|
|
|
|
|
|
|
|
if (adminUser) {
|
|
|
|
throw new BadRequestException('The server already has an admin');
|
|
|
|
}
|
|
|
|
|
2023-08-01 17:49:50 +02:00
|
|
|
const admin = await this.userCore.createUser({
|
|
|
|
isAdmin: true,
|
|
|
|
email: dto.email,
|
|
|
|
firstName: dto.firstName,
|
|
|
|
lastName: dto.lastName,
|
|
|
|
password: dto.password,
|
|
|
|
storageLabel: 'admin',
|
|
|
|
});
|
|
|
|
|
|
|
|
return mapAdminSignupResponse(admin);
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto | null> {
|
2023-01-31 19:11:49 +01:00
|
|
|
const shareKey = (headers['x-immich-share-key'] || params.key) as string;
|
|
|
|
const userToken = (headers['x-immich-user-token'] ||
|
|
|
|
params.userToken ||
|
|
|
|
this.getBearerToken(headers) ||
|
|
|
|
this.getCookieToken(headers)) as string;
|
2023-05-04 18:41:29 +02:00
|
|
|
const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;
|
2023-01-31 19:11:49 +01:00
|
|
|
|
|
|
|
if (shareKey) {
|
2023-06-21 03:08:43 +02:00
|
|
|
return this.validateSharedLink(shareKey);
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-01-31 19:11:49 +01:00
|
|
|
if (userToken) {
|
2023-07-15 06:03:56 +02:00
|
|
|
return this.validateUserToken(userToken);
|
2023-01-31 19:11:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (apiKey) {
|
2023-07-01 03:49:30 +02:00
|
|
|
return this.validateApiKey(apiKey);
|
2023-01-31 19:11:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
throw new UnauthorizedException('Authentication required');
|
|
|
|
}
|
|
|
|
|
2023-04-26 04:19:23 +02:00
|
|
|
async getDevices(authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
|
2023-07-15 06:03:56 +02:00
|
|
|
const userTokens = await this.userTokenRepository.getAll(authUser.id);
|
2023-04-26 04:19:23 +02:00
|
|
|
return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId));
|
|
|
|
}
|
|
|
|
|
|
|
|
async logoutDevice(authUser: AuthUserDto, deviceId: string): Promise<void> {
|
2023-07-15 06:03:56 +02:00
|
|
|
await this.userTokenRepository.delete(authUser.id, deviceId);
|
2023-04-26 04:19:23 +02:00
|
|
|
}
|
|
|
|
|
2023-05-09 21:34:17 +02:00
|
|
|
async logoutDevices(authUser: AuthUserDto): Promise<void> {
|
2023-07-15 06:03:56 +02:00
|
|
|
const devices = await this.userTokenRepository.getAll(authUser.id);
|
2023-05-09 21:34:17 +02:00
|
|
|
for (const device of devices) {
|
|
|
|
if (device.id === authUser.accessTokenId) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-07-15 06:03:56 +02:00
|
|
|
await this.userTokenRepository.delete(authUser.id, device.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getMobileRedirect(url: string) {
|
|
|
|
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
async generateConfig(dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
|
|
|
|
const config = await this.configCore.getConfig();
|
|
|
|
const response = {
|
|
|
|
enabled: config.oauth.enabled,
|
|
|
|
passwordLoginEnabled: config.passwordLogin.enabled,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!response.enabled) {
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { scope, buttonText, autoLaunch } = config.oauth;
|
|
|
|
const url = (await this.getOAuthClient(config)).authorizationUrl({
|
|
|
|
redirect_uri: this.normalize(config, dto.redirectUri),
|
|
|
|
scope,
|
|
|
|
state: generators.state(),
|
|
|
|
});
|
|
|
|
|
|
|
|
return { ...response, buttonText, url, autoLaunch };
|
|
|
|
}
|
|
|
|
|
2023-09-01 13:08:42 +02:00
|
|
|
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
|
|
|
|
const config = await this.configCore.getConfig();
|
|
|
|
if (!config.oauth.enabled) {
|
|
|
|
throw new BadRequestException('OAuth is not enabled');
|
|
|
|
}
|
|
|
|
|
|
|
|
const client = await this.getOAuthClient(config);
|
|
|
|
const url = await client.authorizationUrl({
|
|
|
|
redirect_uri: this.normalize(config, dto.redirectUri),
|
|
|
|
scope: config.oauth.scope,
|
|
|
|
state: generators.state(),
|
|
|
|
});
|
|
|
|
|
|
|
|
return { url };
|
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
async callback(
|
|
|
|
dto: OAuthCallbackDto,
|
|
|
|
loginDetails: LoginDetails,
|
|
|
|
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
|
|
|
|
const config = await this.configCore.getConfig();
|
|
|
|
const profile = await this.getOAuthProfile(config, dto.url);
|
|
|
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
|
|
|
let user = await this.userCore.getByOAuthId(profile.sub);
|
|
|
|
|
|
|
|
// link existing user
|
|
|
|
if (!user) {
|
|
|
|
const emailUser = await this.userCore.getByEmail(profile.email);
|
|
|
|
if (emailUser) {
|
|
|
|
user = await this.userCore.updateUser(emailUser, emailUser.id, { oauthId: profile.sub });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// register new user
|
|
|
|
if (!user) {
|
|
|
|
if (!config.oauth.autoRegister) {
|
|
|
|
this.logger.warn(
|
|
|
|
`Unable to register ${profile.email}. To enable set OAuth Auto Register to true in admin settings.`,
|
|
|
|
);
|
|
|
|
throw new BadRequestException(`User does not exist and auto registering is disabled.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`);
|
2023-07-15 21:50:29 +02:00
|
|
|
this.logger.verbose(`OAuth Profile: ${JSON.stringify(profile)}`);
|
|
|
|
|
|
|
|
let storageLabel: string | null = profile[config.oauth.storageLabelClaim as keyof OAuthProfile] as string;
|
|
|
|
if (typeof storageLabel !== 'string') {
|
|
|
|
storageLabel = null;
|
|
|
|
}
|
|
|
|
|
2023-07-15 06:03:56 +02:00
|
|
|
user = await this.userCore.createUser({
|
|
|
|
firstName: profile.given_name || '',
|
|
|
|
lastName: profile.family_name || '',
|
|
|
|
email: profile.email,
|
|
|
|
oauthId: profile.sub,
|
2023-07-15 21:50:29 +02:00
|
|
|
storageLabel,
|
2023-07-15 06:03:56 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.createLoginResponse(user, AuthType.OAUTH, loginDetails);
|
|
|
|
}
|
|
|
|
|
|
|
|
async link(user: AuthUserDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
|
|
|
|
const config = await this.configCore.getConfig();
|
|
|
|
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
|
|
|
|
const duplicate = await this.userCore.getByOAuthId(oauthId);
|
|
|
|
if (duplicate && duplicate.id !== user.id) {
|
|
|
|
this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);
|
|
|
|
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
|
|
|
}
|
|
|
|
return this.userCore.updateUser(user, user.id, { oauthId });
|
|
|
|
}
|
|
|
|
|
|
|
|
async unlink(user: AuthUserDto): Promise<UserResponseDto> {
|
|
|
|
return this.userCore.updateUser(user, user.id, { oauthId: '' });
|
|
|
|
}
|
|
|
|
|
|
|
|
private async getLogoutEndpoint(authType: AuthType): Promise<string> {
|
|
|
|
if (authType !== AuthType.OAUTH) {
|
|
|
|
return LOGIN_URL;
|
2023-05-09 21:34:17 +02:00
|
|
|
}
|
2023-07-15 06:03:56 +02:00
|
|
|
|
|
|
|
const config = await this.configCore.getConfig();
|
|
|
|
if (!config.oauth.enabled) {
|
|
|
|
return LOGIN_URL;
|
|
|
|
}
|
|
|
|
|
|
|
|
const client = await this.getOAuthClient(config);
|
|
|
|
return client.issuer.metadata.end_session_endpoint || LOGIN_URL;
|
|
|
|
}
|
|
|
|
|
|
|
|
private async getOAuthProfile(config: SystemConfig, url: string): Promise<OAuthProfile> {
|
|
|
|
const redirectUri = this.normalize(config, url.split('?')[0]);
|
|
|
|
const client = await this.getOAuthClient(config);
|
|
|
|
const params = client.callbackParams(url);
|
2023-09-01 13:08:42 +02:00
|
|
|
try {
|
|
|
|
const tokens = await client.callback(redirectUri, params, { state: params.state });
|
|
|
|
return client.userinfo<OAuthProfile>(tokens.access_token || '');
|
|
|
|
} catch (error: Error | any) {
|
|
|
|
this.logger.error(`Unable to complete OAuth login: ${error}`, error?.stack);
|
|
|
|
throw new InternalServerErrorException(`Unable to complete OAuth login: ${error}`, { cause: error });
|
|
|
|
}
|
2023-07-15 06:03:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private async getOAuthClient(config: SystemConfig) {
|
|
|
|
const { enabled, clientId, clientSecret, issuerUrl } = config.oauth;
|
|
|
|
|
|
|
|
if (!enabled) {
|
|
|
|
throw new BadRequestException('OAuth2 is not enabled');
|
|
|
|
}
|
|
|
|
|
|
|
|
const metadata: ClientMetadata = {
|
|
|
|
client_id: clientId,
|
|
|
|
client_secret: clientSecret,
|
|
|
|
response_types: ['code'],
|
|
|
|
};
|
|
|
|
|
|
|
|
const issuer = await Issuer.discover(issuerUrl);
|
|
|
|
const algorithms = (issuer.id_token_signing_alg_values_supported || []) as string[];
|
|
|
|
if (algorithms[0] === 'HS256') {
|
|
|
|
metadata.id_token_signed_response_alg = algorithms[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
return new issuer.Client(metadata);
|
|
|
|
}
|
|
|
|
|
|
|
|
private normalize(config: SystemConfig, redirectUri: string) {
|
|
|
|
const isMobile = redirectUri.startsWith(MOBILE_REDIRECT);
|
|
|
|
const { mobileRedirectUri, mobileOverrideEnabled } = config.oauth;
|
|
|
|
if (isMobile && mobileOverrideEnabled && mobileRedirectUri) {
|
|
|
|
return mobileRedirectUri;
|
|
|
|
}
|
|
|
|
return redirectUri;
|
2023-05-09 21:34:17 +02:00
|
|
|
}
|
|
|
|
|
2023-01-31 19:11:49 +01:00
|
|
|
private getBearerToken(headers: IncomingHttpHeaders): string | null {
|
|
|
|
const [type, token] = (headers.authorization || '').split(' ');
|
|
|
|
if (type.toLowerCase() === 'bearer') {
|
|
|
|
return token;
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-01-28 06:12:11 +01:00
|
|
|
return null;
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
|
|
|
|
2023-01-31 19:11:49 +01:00
|
|
|
private getCookieToken(headers: IncomingHttpHeaders): string | null {
|
|
|
|
const cookies = cookieParser.parse(headers.cookie || '');
|
|
|
|
return cookies[IMMICH_ACCESS_COOKIE] || null;
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|
2023-06-21 03:08:43 +02:00
|
|
|
|
2023-07-01 03:49:30 +02:00
|
|
|
private async validateSharedLink(key: string | string[]): Promise<AuthUserDto> {
|
2023-06-21 03:08:43 +02:00
|
|
|
key = Array.isArray(key) ? key[0] : key;
|
|
|
|
|
|
|
|
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
|
|
|
const link = await this.sharedLinkRepository.getByKey(bytes);
|
|
|
|
if (link) {
|
|
|
|
if (!link.expiresAt || new Date(link.expiresAt) > new Date()) {
|
|
|
|
const user = link.user;
|
|
|
|
if (user) {
|
|
|
|
return {
|
|
|
|
id: user.id,
|
|
|
|
email: user.email,
|
|
|
|
isAdmin: user.isAdmin,
|
|
|
|
isPublicUser: true,
|
|
|
|
sharedLinkId: link.id,
|
|
|
|
isAllowUpload: link.allowUpload,
|
|
|
|
isAllowDownload: link.allowDownload,
|
|
|
|
isShowExif: link.showExif,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new UnauthorizedException('Invalid share key');
|
|
|
|
}
|
2023-07-01 03:49:30 +02:00
|
|
|
|
|
|
|
private async validateApiKey(key: string): Promise<AuthUserDto> {
|
|
|
|
const hashedKey = this.cryptoRepository.hashSha256(key);
|
|
|
|
const keyEntity = await this.keyRepository.getKey(hashedKey);
|
|
|
|
if (keyEntity?.user) {
|
|
|
|
const user = keyEntity.user;
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: user.id,
|
|
|
|
email: user.email,
|
|
|
|
isAdmin: user.isAdmin,
|
|
|
|
isPublicUser: false,
|
|
|
|
isAllowUpload: true,
|
|
|
|
externalPath: user.externalPath,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new UnauthorizedException('Invalid API key');
|
|
|
|
}
|
2023-07-15 06:03:56 +02:00
|
|
|
|
|
|
|
private validatePassword(inputPassword: string, user: UserEntity): boolean {
|
|
|
|
if (!user || !user.password) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async validateUserToken(tokenValue: string): Promise<AuthUserDto> {
|
|
|
|
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
|
|
|
|
let token = await this.userTokenRepository.getByToken(hashedToken);
|
|
|
|
|
|
|
|
if (token?.user) {
|
|
|
|
const now = DateTime.now();
|
|
|
|
const updatedAt = DateTime.fromJSDate(token.updatedAt);
|
|
|
|
const diff = now.diff(updatedAt, ['hours']);
|
|
|
|
if (diff.hours > 1) {
|
|
|
|
token = await this.userTokenRepository.save({ ...token, updatedAt: new Date() });
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...token.user,
|
|
|
|
isPublicUser: false,
|
|
|
|
isAllowUpload: true,
|
|
|
|
isAllowDownload: true,
|
|
|
|
isShowExif: true,
|
|
|
|
accessTokenId: token.id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new UnauthorizedException('Invalid user token');
|
|
|
|
}
|
|
|
|
|
|
|
|
private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) {
|
|
|
|
const key = this.cryptoRepository.randomBytes(32).toString('base64').replace(/\W/g, '');
|
|
|
|
const token = this.cryptoRepository.hashSha256(key);
|
|
|
|
|
|
|
|
await this.userTokenRepository.create({
|
|
|
|
token,
|
|
|
|
user,
|
|
|
|
deviceOS: loginDetails.deviceOS,
|
|
|
|
deviceType: loginDetails.deviceType,
|
|
|
|
});
|
|
|
|
|
|
|
|
const response = mapLoginResponse(user, key);
|
|
|
|
const cookie = this.getCookies(response, authType, loginDetails);
|
|
|
|
return { response, cookie };
|
|
|
|
}
|
|
|
|
|
|
|
|
private getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) {
|
|
|
|
const maxAge = 400 * 24 * 3600; // 400 days
|
|
|
|
|
|
|
|
let authTypeCookie = '';
|
|
|
|
let accessTokenCookie = '';
|
|
|
|
|
|
|
|
if (isSecure) {
|
|
|
|
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
|
|
|
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
|
|
|
} else {
|
|
|
|
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
|
|
|
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
|
|
|
}
|
|
|
|
return [accessTokenCookie, authTypeCookie];
|
|
|
|
}
|
2023-01-24 05:13:42 +01:00
|
|
|
}
|