refactor(server): narrow auth types (#16066)

This commit is contained in:
Jason Rasmussen
2025-02-12 15:23:08 -05:00
committed by GitHub
parent 7c821dd205
commit 2d7c333c8c
25 changed files with 265 additions and 239 deletions

View File

@@ -17,12 +17,10 @@ import {
mapLoginResponse,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { SessionEntity } from 'src/entities/session.entity';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
import { OAuthProfile } from 'src/repositories/oauth.repository';
import { BaseService } from 'src/services/base.service';
import { AuthApiKey } from 'src/types';
import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes';
@@ -298,11 +296,11 @@ export class AuthService extends BaseService {
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
if (sharedLink && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) {
const user = sharedLink.user;
if (user) {
return { user, sharedLink };
}
if (sharedLink?.user && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) {
return {
user: sharedLink.user,
sharedLink,
};
}
throw new UnauthorizedException('Invalid share key');
}
@@ -310,10 +308,10 @@ export class AuthService extends BaseService {
private async validateApiKey(key: string): Promise<AuthDto> {
const hashedKey = this.cryptoRepository.hashSha256(key);
const apiKey = await this.keyRepository.getKey(hashedKey);
if (apiKey) {
if (apiKey?.user) {
return {
user: apiKey.user as unknown as UserEntity,
apiKey: apiKey as unknown as AuthApiKey,
user: apiKey.user,
apiKey,
};
}
@@ -330,7 +328,6 @@ export class AuthService extends BaseService {
private async validateSession(tokenValue: string): Promise<AuthDto> {
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
const session = await this.sessionRepository.getByToken(hashedToken);
if (session?.user) {
const now = DateTime.now();
const updatedAt = DateTime.fromJSDate(session.updatedAt);
@@ -339,7 +336,10 @@ export class AuthService extends BaseService {
await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
}
return { user: session.user as unknown as UserEntity, session: session as unknown as SessionEntity };
return {
user: session.user,
session,
};
}
throw new UnauthorizedException('Invalid user token');

View File

@@ -19,7 +19,8 @@ export class DownloadService extends BaseService {
const archives: DownloadArchiveInfo[] = [];
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
const preferences = getPreferences(auth.user);
const metadata = await this.userRepository.getMetadata(auth.user.id);
const preferences = getPreferences(auth.user.email, metadata);
const assetPagination = await this.getDownloadAssets(auth, dto);
for await (const assets of assetPagination) {

View File

@@ -276,7 +276,7 @@ export class NotificationService extends BaseService {
return JobStatus.SKIPPED;
}
const { emailNotifications } = getPreferences(recipient);
const { emailNotifications } = getPreferences(recipient.email, recipient.metadata);
if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
return JobStatus.SKIPPED;
@@ -340,7 +340,7 @@ export class NotificationService extends BaseService {
continue;
}
const { emailNotifications } = getPreferences(user);
const { emailNotifications } = getPreferences(user.email, user.metadata);
if (!emailNotifications.enabled || !emailNotifications.albumUpdate) {
continue;

View File

@@ -106,21 +106,24 @@ export class UserAdminService extends BaseService {
}
async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> {
const user = await this.findOrFail(id, { withDeleted: false });
const preferences = getPreferences(user);
const { email } = await this.findOrFail(id, { withDeleted: true });
const metadata = await this.userRepository.getMetadata(id);
const preferences = getPreferences(email, metadata);
return mapPreferences(preferences);
}
async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) {
const user = await this.findOrFail(id, { withDeleted: false });
const preferences = mergePreferences(user, dto);
const { email } = await this.findOrFail(id, { withDeleted: false });
const metadata = await this.userRepository.getMetadata(id);
const preferences = getPreferences(email, metadata);
const newPreferences = mergePreferences(preferences, dto);
await this.userRepository.upsertMetadata(user.id, {
await this.userRepository.upsertMetadata(id, {
key: UserMetadataKey.PREFERENCES,
value: getPreferencesPartial(user, preferences),
value: getPreferencesPartial({ email }, newPreferences),
});
return mapPreferences(preferences);
return mapPreferences(newPreferences);
}
private async findOrFail(id: string, options: UserFindOptions) {

View File

@@ -77,9 +77,9 @@ describe(UserService.name, () => {
});
describe('getMe', () => {
it("should get the auth user's info", () => {
it("should get the auth user's info", async () => {
const user = authStub.admin.user;
expect(sut.getMe(authStub.admin)).toMatchObject({
await expect(sut.getMe(authStub.admin)).resolves.toMatchObject({
id: user.id,
email: user.email,
});

View File

@@ -22,16 +22,24 @@ export class UserService extends BaseService {
async search(auth: AuthDto): Promise<UserResponseDto[]> {
const config = await this.getConfig({ withCache: false });
let users: UserEntity[] = [auth.user];
let users;
if (auth.user.isAdmin || config.server.publicUsers) {
users = await this.userRepository.getList({ withDeleted: false });
} else {
const authUser = await this.userRepository.get(auth.user.id, {});
users = authUser ? [authUser] : [];
}
return users.map((user) => mapUser(user));
}
getMe(auth: AuthDto): UserAdminResponseDto {
return mapUserAdmin(auth.user);
async getMe(auth: AuthDto): Promise<UserAdminResponseDto> {
const user = await this.userRepository.get(auth.user.id, {});
if (!user) {
throw new BadRequestException('User not found');
}
return mapUserAdmin(user);
}
async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
@@ -58,20 +66,23 @@ export class UserService extends BaseService {
return mapUserAdmin(updatedUser);
}
getMyPreferences({ user }: AuthDto): UserPreferencesResponseDto {
const preferences = getPreferences(user);
async getMyPreferences(auth: AuthDto): Promise<UserPreferencesResponseDto> {
const metadata = await this.userRepository.getMetadata(auth.user.id);
const preferences = getPreferences(auth.user.email, metadata);
return mapPreferences(preferences);
}
async updateMyPreferences({ user }: AuthDto, dto: UserPreferencesUpdateDto) {
const preferences = mergePreferences(user, dto);
async updateMyPreferences(auth: AuthDto, dto: UserPreferencesUpdateDto) {
const metadata = await this.userRepository.getMetadata(auth.user.id);
const current = getPreferences(auth.user.email, metadata);
const updated = mergePreferences(current, dto);
await this.userRepository.upsertMetadata(user.id, {
await this.userRepository.upsertMetadata(auth.user.id, {
key: UserMetadataKey.PREFERENCES,
value: getPreferencesPartial(user, preferences),
value: getPreferencesPartial(auth.user, updated),
});
return mapPreferences(preferences);
return mapPreferences(updated);
}
async get(id: string): Promise<UserResponseDto> {
@@ -120,8 +131,10 @@ export class UserService extends BaseService {
});
}
getLicense({ user }: AuthDto): LicenseResponseDto {
const license = user.metadata.find(
async getLicense(auth: AuthDto): Promise<LicenseResponseDto> {
const metadata = await this.userRepository.getMetadata(auth.user.id);
const license = metadata.find(
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
);
if (!license) {