mirror of
https://github.com/immich-app/immich
synced 2026-08-22 13:13:05 +00:00
* fix: shared check for server setup availability * chore: add medium test * feat: require @Authenticated decorator everywhere * fix: lints
124 lines
4.2 KiB
TypeScript
124 lines
4.2 KiB
TypeScript
import {
|
|
CanActivate,
|
|
ExecutionContext,
|
|
Injectable,
|
|
SetMetadata,
|
|
applyDecorators,
|
|
createParamDecorator,
|
|
} from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { ApiBearerAuth, ApiCookieAuth, ApiExtension, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
|
import { Request } from 'express';
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
|
import { ApiCustomExtension, ImmichQuery, MetadataKey, Permission } from 'src/enum';
|
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
|
import { getUserAgentDetails } from 'src/utils/request';
|
|
|
|
type AdminRoute = { admin?: true };
|
|
type SharedLinkRoute = { sharedLink?: true };
|
|
type AuthorizedRoute = { permission?: Permission | false; public?: never; setup?: never } & (
|
|
AdminRoute | SharedLinkRoute
|
|
);
|
|
type PublicRoute = { public: true; setup?: true; permission?: never; admin?: never; sharedLink?: never };
|
|
export type AuthenticatedOptions = AuthorizedRoute | PublicRoute;
|
|
|
|
type ReflectorTarget = Parameters<Reflector['get']>[1];
|
|
|
|
/** Resolves the `@Authenticated()` options of a route handler, with the defaults applied. */
|
|
export const getAuthenticatedOptions = (reflector: Reflector, target: ReflectorTarget) => {
|
|
const options = reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AuthRoute, [target]);
|
|
return options && { sharedLink: false, admin: false, public: false, setup: false, ...options };
|
|
};
|
|
|
|
export const Authenticated = (options: AuthenticatedOptions = {}): MethodDecorator => {
|
|
const decorators: MethodDecorator[] = [SetMetadata(MetadataKey.AuthRoute, options)];
|
|
|
|
if (!options.public) {
|
|
decorators.push(ApiBearerAuth(), ApiCookieAuth(), ApiSecurity(MetadataKey.ApiKeySecurity));
|
|
}
|
|
|
|
if ((options as AdminRoute).admin) {
|
|
decorators.push(ApiExtension(ApiCustomExtension.AdminOnly, true));
|
|
}
|
|
|
|
if (options?.permission) {
|
|
decorators.push(ApiExtension(ApiCustomExtension.Permission, options.permission));
|
|
}
|
|
|
|
if ((options as SharedLinkRoute)?.sharedLink) {
|
|
decorators.push(
|
|
ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false }),
|
|
ApiQuery({ name: ImmichQuery.SharedLinkSlug, type: String, required: false }),
|
|
);
|
|
}
|
|
|
|
return applyDecorators(...decorators);
|
|
};
|
|
|
|
export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => {
|
|
return context.switchToHttp().getRequest<AuthenticatedRequest>().user;
|
|
});
|
|
|
|
export const FileResponse = () =>
|
|
ApiOkResponse({
|
|
content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } },
|
|
});
|
|
|
|
export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => {
|
|
const request = context.switchToHttp().getRequest<Request>();
|
|
const { deviceType, deviceOS, appVersion } = getUserAgentDetails(request.headers);
|
|
|
|
return {
|
|
clientIp: request.ip ?? '',
|
|
isSecure: request.secure,
|
|
deviceType,
|
|
deviceOS,
|
|
appVersion,
|
|
};
|
|
});
|
|
|
|
export interface AuthRequest extends Request {
|
|
user?: AuthDto;
|
|
}
|
|
|
|
export interface AuthenticatedRequest extends Request {
|
|
user: AuthDto;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AuthGuard implements CanActivate {
|
|
constructor(
|
|
private logger: LoggingRepository,
|
|
private reflector: Reflector,
|
|
private authService: AuthService,
|
|
) {
|
|
this.logger.setContext(AuthGuard.name);
|
|
}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const options = getAuthenticatedOptions(this.reflector, context.getHandler());
|
|
if (!options) {
|
|
throw new Error(`Route ${context.getHandler().name} does not declare @Authenticated()`);
|
|
}
|
|
|
|
if (options.setup) {
|
|
await this.authService.requireSetupAvailable();
|
|
}
|
|
|
|
if (options.public) {
|
|
return true;
|
|
}
|
|
|
|
const { admin: adminRoute, sharedLink: sharedLinkRoute, permission } = options;
|
|
const request = context.switchToHttp().getRequest<AuthRequest>();
|
|
|
|
request.user = await this.authService.authenticate({
|
|
headers: request.headers,
|
|
queryParams: request.query as Record<string, string>,
|
|
metadata: { adminRoute, sharedLinkRoute, permission, uri: request.path },
|
|
});
|
|
|
|
return true;
|
|
}
|
|
}
|