This commit is contained in:
Jason Rasmussen 2025-10-15 22:04:39 -07:00 committed by GitHub
commit 2282edf9ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 143 additions and 43 deletions

View file

@ -39,6 +39,17 @@ dynamic upgradeDto(dynamic value, String targetType) {
case 'LoginResponseDto': case 'LoginResponseDto':
if (value is Map) { if (value is Map) {
addDefault(value, 'isOnboarded', false); addDefault(value, 'isOnboarded', false);
addDefault(value, 'permissions', ['all']);
}
break;
case 'SessionResponseDto':
if (value is Map) {
addDefault(value, 'permissions', ['all']);
}
break;
case 'SessionCreateResponseDto':
if (value is Map) {
addDefault(value, 'permissions', ['all']);
} }
break; break;
case 'SyncUserV1': case 'SyncUserV1':

View file

@ -17,6 +17,7 @@ class LoginResponseDto {
required this.isAdmin, required this.isAdmin,
required this.isOnboarded, required this.isOnboarded,
required this.name, required this.name,
this.permissions = const [],
required this.profileImagePath, required this.profileImagePath,
required this.shouldChangePassword, required this.shouldChangePassword,
required this.userEmail, required this.userEmail,
@ -31,6 +32,8 @@ class LoginResponseDto {
String name; String name;
List<Permission> permissions;
String profileImagePath; String profileImagePath;
bool shouldChangePassword; bool shouldChangePassword;
@ -45,6 +48,7 @@ class LoginResponseDto {
other.isAdmin == isAdmin && other.isAdmin == isAdmin &&
other.isOnboarded == isOnboarded && other.isOnboarded == isOnboarded &&
other.name == name && other.name == name &&
_deepEquality.equals(other.permissions, permissions) &&
other.profileImagePath == profileImagePath && other.profileImagePath == profileImagePath &&
other.shouldChangePassword == shouldChangePassword && other.shouldChangePassword == shouldChangePassword &&
other.userEmail == userEmail && other.userEmail == userEmail &&
@ -57,13 +61,14 @@ class LoginResponseDto {
(isAdmin.hashCode) + (isAdmin.hashCode) +
(isOnboarded.hashCode) + (isOnboarded.hashCode) +
(name.hashCode) + (name.hashCode) +
(permissions.hashCode) +
(profileImagePath.hashCode) + (profileImagePath.hashCode) +
(shouldChangePassword.hashCode) + (shouldChangePassword.hashCode) +
(userEmail.hashCode) + (userEmail.hashCode) +
(userId.hashCode); (userId.hashCode);
@override @override
String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, permissions=$permissions, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -71,6 +76,7 @@ class LoginResponseDto {
json[r'isAdmin'] = this.isAdmin; json[r'isAdmin'] = this.isAdmin;
json[r'isOnboarded'] = this.isOnboarded; json[r'isOnboarded'] = this.isOnboarded;
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'permissions'] = this.permissions;
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
json[r'shouldChangePassword'] = this.shouldChangePassword; json[r'shouldChangePassword'] = this.shouldChangePassword;
json[r'userEmail'] = this.userEmail; json[r'userEmail'] = this.userEmail;
@ -91,6 +97,7 @@ class LoginResponseDto {
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!, isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
isOnboarded: mapValueOfType<bool>(json, r'isOnboarded')!, isOnboarded: mapValueOfType<bool>(json, r'isOnboarded')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
permissions: Permission.listFromJson(json[r'permissions']),
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!, shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
userEmail: mapValueOfType<String>(json, r'userEmail')!, userEmail: mapValueOfType<String>(json, r'userEmail')!,
@ -146,6 +153,7 @@ class LoginResponseDto {
'isAdmin', 'isAdmin',
'isOnboarded', 'isOnboarded',
'name', 'name',
'permissions',
'profileImagePath', 'profileImagePath',
'shouldChangePassword', 'shouldChangePassword',
'userEmail', 'userEmail',

View file

@ -20,6 +20,7 @@ class SessionCreateResponseDto {
this.expiresAt, this.expiresAt,
required this.id, required this.id,
required this.isPendingSyncReset, required this.isPendingSyncReset,
this.permissions = const [],
required this.token, required this.token,
required this.updatedAt, required this.updatedAt,
}); });
@ -44,6 +45,8 @@ class SessionCreateResponseDto {
bool isPendingSyncReset; bool isPendingSyncReset;
List<Permission> permissions;
String token; String token;
String updatedAt; String updatedAt;
@ -57,6 +60,7 @@ class SessionCreateResponseDto {
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.id == id && other.id == id &&
other.isPendingSyncReset == isPendingSyncReset && other.isPendingSyncReset == isPendingSyncReset &&
_deepEquality.equals(other.permissions, permissions) &&
other.token == token && other.token == token &&
other.updatedAt == updatedAt; other.updatedAt == updatedAt;
@ -70,11 +74,12 @@ class SessionCreateResponseDto {
(expiresAt == null ? 0 : expiresAt!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isPendingSyncReset.hashCode) + (isPendingSyncReset.hashCode) +
(permissions.hashCode) +
(token.hashCode) + (token.hashCode) +
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]'; String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, permissions=$permissions, token=$token, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -89,6 +94,7 @@ class SessionCreateResponseDto {
} }
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isPendingSyncReset'] = this.isPendingSyncReset; json[r'isPendingSyncReset'] = this.isPendingSyncReset;
json[r'permissions'] = this.permissions;
json[r'token'] = this.token; json[r'token'] = this.token;
json[r'updatedAt'] = this.updatedAt; json[r'updatedAt'] = this.updatedAt;
return json; return json;
@ -110,6 +116,7 @@ class SessionCreateResponseDto {
expiresAt: mapValueOfType<String>(json, r'expiresAt'), expiresAt: mapValueOfType<String>(json, r'expiresAt'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!, isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
permissions: Permission.listFromJson(json[r'permissions']),
token: mapValueOfType<String>(json, r'token')!, token: mapValueOfType<String>(json, r'token')!,
updatedAt: mapValueOfType<String>(json, r'updatedAt')!, updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
); );
@ -165,6 +172,7 @@ class SessionCreateResponseDto {
'deviceType', 'deviceType',
'id', 'id',
'isPendingSyncReset', 'isPendingSyncReset',
'permissions',
'token', 'token',
'updatedAt', 'updatedAt',
}; };

View file

@ -20,6 +20,7 @@ class SessionResponseDto {
this.expiresAt, this.expiresAt,
required this.id, required this.id,
required this.isPendingSyncReset, required this.isPendingSyncReset,
this.permissions = const [],
required this.updatedAt, required this.updatedAt,
}); });
@ -43,6 +44,8 @@ class SessionResponseDto {
bool isPendingSyncReset; bool isPendingSyncReset;
List<Permission> permissions;
String updatedAt; String updatedAt;
@override @override
@ -54,6 +57,7 @@ class SessionResponseDto {
other.expiresAt == expiresAt && other.expiresAt == expiresAt &&
other.id == id && other.id == id &&
other.isPendingSyncReset == isPendingSyncReset && other.isPendingSyncReset == isPendingSyncReset &&
_deepEquality.equals(other.permissions, permissions) &&
other.updatedAt == updatedAt; other.updatedAt == updatedAt;
@override @override
@ -66,10 +70,11 @@ class SessionResponseDto {
(expiresAt == null ? 0 : expiresAt!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isPendingSyncReset.hashCode) + (isPendingSyncReset.hashCode) +
(permissions.hashCode) +
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]'; String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, permissions=$permissions, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -84,6 +89,7 @@ class SessionResponseDto {
} }
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isPendingSyncReset'] = this.isPendingSyncReset; json[r'isPendingSyncReset'] = this.isPendingSyncReset;
json[r'permissions'] = this.permissions;
json[r'updatedAt'] = this.updatedAt; json[r'updatedAt'] = this.updatedAt;
return json; return json;
} }
@ -104,6 +110,7 @@ class SessionResponseDto {
expiresAt: mapValueOfType<String>(json, r'expiresAt'), expiresAt: mapValueOfType<String>(json, r'expiresAt'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!, isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
permissions: Permission.listFromJson(json[r'permissions']),
updatedAt: mapValueOfType<String>(json, r'updatedAt')!, updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
); );
} }
@ -158,6 +165,7 @@ class SessionResponseDto {
'deviceType', 'deviceType',
'id', 'id',
'isPendingSyncReset', 'isPendingSyncReset',
'permissions',
'updatedAt', 'updatedAt',
}; };
} }

View file

@ -12240,6 +12240,12 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"permissions": {
"items": {
"$ref": "#/components/schemas/Permission"
},
"type": "array"
},
"profileImagePath": { "profileImagePath": {
"type": "string" "type": "string"
}, },
@ -12258,6 +12264,7 @@
"isAdmin", "isAdmin",
"isOnboarded", "isOnboarded",
"name", "name",
"permissions",
"profileImagePath", "profileImagePath",
"shouldChangePassword", "shouldChangePassword",
"userEmail", "userEmail",
@ -14324,6 +14331,12 @@
"isPendingSyncReset": { "isPendingSyncReset": {
"type": "boolean" "type": "boolean"
}, },
"permissions": {
"items": {
"$ref": "#/components/schemas/Permission"
},
"type": "array"
},
"token": { "token": {
"type": "string" "type": "string"
}, },
@ -14338,6 +14351,7 @@
"deviceType", "deviceType",
"id", "id",
"isPendingSyncReset", "isPendingSyncReset",
"permissions",
"token", "token",
"updatedAt" "updatedAt"
], ],
@ -14366,6 +14380,12 @@
"isPendingSyncReset": { "isPendingSyncReset": {
"type": "boolean" "type": "boolean"
}, },
"permissions": {
"items": {
"$ref": "#/components/schemas/Permission"
},
"type": "array"
},
"updatedAt": { "updatedAt": {
"type": "string" "type": "string"
} }
@ -14377,6 +14397,7 @@
"deviceType", "deviceType",
"id", "id",
"isPendingSyncReset", "isPendingSyncReset",
"permissions",
"updatedAt" "updatedAt"
], ],
"type": "object" "type": "object"

View file

@ -561,6 +561,7 @@ export type LoginResponseDto = {
isAdmin: boolean; isAdmin: boolean;
isOnboarded: boolean; isOnboarded: boolean;
name: string; name: string;
permissions: Permission[];
profileImagePath: string; profileImagePath: string;
shouldChangePassword: boolean; shouldChangePassword: boolean;
userEmail: string; userEmail: string;
@ -1199,6 +1200,7 @@ export type SessionResponseDto = {
expiresAt?: string; expiresAt?: string;
id: string; id: string;
isPendingSyncReset: boolean; isPendingSyncReset: boolean;
permissions: Permission[];
updatedAt: string; updatedAt: string;
}; };
export type SessionCreateDto = { export type SessionCreateDto = {
@ -1215,6 +1217,7 @@ export type SessionCreateResponseDto = {
expiresAt?: string; expiresAt?: string;
id: string; id: string;
isPendingSyncReset: boolean; isPendingSyncReset: boolean;
permissions: Permission[];
token: string; token: string;
updatedAt: string; updatedAt: string;
}; };

View file

@ -203,6 +203,7 @@ export type Album = Selectable<AlbumTable> & {
export type AuthSession = { export type AuthSession = {
id: string; id: string;
hasElevatedPermission: boolean; hasElevatedPermission: boolean;
permissions: Permission[];
}; };
export type Partner = { export type Partner = {
@ -240,6 +241,7 @@ export type Session = {
deviceType: string; deviceType: string;
pinExpiresAt: Date | null; pinExpiresAt: Date | null;
isPendingSyncReset: boolean; isPendingSyncReset: boolean;
permissions: Permission[];
}; };
export type Exif = Omit<Selectable<AssetExifTable>, 'updatedAt' | 'updateId'>; export type Exif = Omit<Selectable<AssetExifTable>, 'updatedAt' | 'updateId'>;
@ -308,7 +310,7 @@ export const columns = {
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
authApiKey: ['api_key.id', 'api_key.permissions'], authApiKey: ['api_key.id', 'api_key.permissions'],
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt'], authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.permissions'],
authSharedLink: [ authSharedLink: [
'shared_link.id', 'shared_link.id',
'shared_link.userId', 'shared_link.userId',

View file

@ -1,10 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser } from 'src/database';
import { ImmichCookie, UserMetadataKey } from 'src/enum'; import { ImmichCookie, Permission } from 'src/enum';
import { UserMetadataItem } from 'src/types'; import { Optional, PinCode, toEmail, ValidateEnum } from 'src/validation';
import { Optional, PinCode, toEmail } from 'src/validation';
export type CookieResponse = { export type CookieResponse = {
isSecure: boolean; isSecure: boolean;
@ -41,23 +40,8 @@ export class LoginResponseDto {
isAdmin!: boolean; isAdmin!: boolean;
shouldChangePassword!: boolean; shouldChangePassword!: boolean;
isOnboarded!: boolean; isOnboarded!: boolean;
} @ValidateEnum({ enum: Permission, name: 'Permission', each: true })
permissions!: Permission[];
export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto {
const onboardingMetadata = entity.metadata.find(
(item): item is UserMetadataItem<UserMetadataKey.Onboarding> => item.key === UserMetadataKey.Onboarding,
)?.value;
return {
accessToken,
userId: entity.id,
userEmail: entity.email,
name: entity.name,
isAdmin: entity.isAdmin,
profileImagePath: entity.profileImagePath,
shouldChangePassword: entity.shouldChangePassword,
isOnboarded: onboardingMetadata?.isOnboarded ?? false,
};
} }
export class LogoutResponseDto { export class LogoutResponseDto {

View file

@ -1,6 +1,7 @@
import { Equals, IsInt, IsPositive, IsString } from 'class-validator'; import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
import { Session } from 'src/database'; import { Session } from 'src/database';
import { Optional, ValidateBoolean } from 'src/validation'; import { Permission } from 'src/enum';
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
export class SessionCreateDto { export class SessionCreateDto {
/** /**
@ -35,6 +36,8 @@ export class SessionResponseDto {
deviceType!: string; deviceType!: string;
deviceOS!: string; deviceOS!: string;
isPendingSyncReset!: boolean; isPendingSyncReset!: boolean;
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
permissions!: Permission[];
} }
export class SessionCreateResponseDto extends SessionResponseDto { export class SessionCreateResponseDto extends SessionResponseDto {
@ -50,4 +53,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
deviceOS: entity.deviceOS, deviceOS: entity.deviceOS,
deviceType: entity.deviceType, deviceType: entity.deviceType,
isPendingSyncReset: entity.isPendingSyncReset, isPendingSyncReset: entity.isPendingSyncReset,
permissions: entity.permissions,
}); });

View file

@ -4,7 +4,8 @@
select select
"id", "id",
"expiresAt", "expiresAt",
"pinExpiresAt" "pinExpiresAt",
"permissions"
from from
"session" "session"
where where
@ -23,6 +24,7 @@ select
"session"."id", "session"."id",
"session"."updatedAt", "session"."updatedAt",
"session"."pinExpiresAt", "session"."pinExpiresAt",
"session"."permissions",
( (
select select
to_json(obj) to_json(obj)

View file

@ -32,7 +32,7 @@ export class SessionRepository {
get(id: string) { get(id: string) {
return this.db return this.db
.selectFrom('session') .selectFrom('session')
.select(['id', 'expiresAt', 'pinExpiresAt']) .select(['id', 'expiresAt', 'pinExpiresAt', 'permissions'])
.where('id', '=', id) .where('id', '=', id)
.executeTakeFirst(); .executeTakeFirst();
} }

View file

@ -0,0 +1,11 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "session" ADD "permissions" character varying[];`.execute(db);
await sql`UPDATE "session" SET "permissions" = ARRAY['all'];`.execute(db);
await sql`ALTER TABLE "session" ALTER COLUMN "permissions" SET NOT NULL`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "session" DROP COLUMN "permissions";`.execute(db);
}

View file

@ -1,4 +1,5 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { Permission } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table'; import { UserTable } from 'src/schema/tables/user.table';
import { import {
Column, Column,
@ -50,4 +51,7 @@ export class SessionTable {
@Column({ type: 'timestamp with time zone', nullable: true }) @Column({ type: 'timestamp with time zone', nullable: true })
pinExpiresAt!: Timestamp | null; pinExpiresAt!: Timestamp | null;
@Column({ array: true, type: 'character varying' })
permissions!: Permission[];
} }

View file

@ -30,6 +30,7 @@ const oauthResponse = ({
isAdmin: false, isAdmin: false,
isOnboarded: false, isOnboarded: false,
shouldChangePassword: false, shouldChangePassword: false,
permissions: [Permission.All],
}); });
// const token = Buffer.from('my-api-key', 'utf8').toString('base64'); // const token = Buffer.from('my-api-key', 'utf8').toString('base64');
@ -104,6 +105,7 @@ describe(AuthService.name, () => {
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isOnboarded: false, isOnboarded: false,
shouldChangePassword: user.shouldChangePassword, shouldChangePassword: user.shouldChangePassword,
permissions: [Permission.All],
}); });
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);

View file

@ -12,6 +12,7 @@ import {
AuthStatusResponseDto, AuthStatusResponseDto,
ChangePasswordDto, ChangePasswordDto,
LoginCredentialDto, LoginCredentialDto,
LoginResponseDto,
LogoutResponseDto, LogoutResponseDto,
OAuthCallbackDto, OAuthCallbackDto,
OAuthConfigDto, OAuthConfigDto,
@ -20,12 +21,21 @@ import {
PinCodeSetupDto, PinCodeSetupDto,
SessionUnlockDto, SessionUnlockDto,
SignUpDto, SignUpDto,
mapLoginResponse,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, JobName, Permission, StorageFolder } from 'src/enum'; import {
AuthType,
ImmichCookie,
ImmichHeader,
ImmichQuery,
JobName,
Permission,
StorageFolder,
UserMetadataKey,
} from 'src/enum';
import { OAuthProfile } from 'src/repositories/oauth.repository'; import { OAuthProfile } from 'src/repositories/oauth.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { UserMetadataItem } from 'src/types';
import { isGranted } from 'src/utils/access'; import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
@ -75,7 +85,7 @@ export class AuthService extends BaseService {
throw new UnauthorizedException('Incorrect email or password'); throw new UnauthorizedException('Incorrect email or password');
} }
return this.createLoginResponse(user, details); return this.createLoginResponse(user, details, [Permission.All]);
} }
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> { async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
@ -177,6 +187,7 @@ export class AuthService extends BaseService {
const authDto = await this.validate({ headers, queryParams }); const authDto = await this.validate({ headers, queryParams });
const { adminRoute, sharedLinkRoute, uri } = metadata; const { adminRoute, sharedLinkRoute, uri } = metadata;
const requestedPermission = metadata.permission ?? Permission.All; const requestedPermission = metadata.permission ?? Permission.All;
const currentPermissions = authDto.apiKey?.permissions || authDto.session?.permissions;
if (!authDto.user.isAdmin && adminRoute) { if (!authDto.user.isAdmin && adminRoute) {
this.logger.warn(`Denied access to admin only route: ${uri}`); this.logger.warn(`Denied access to admin only route: ${uri}`);
@ -189,9 +200,9 @@ export class AuthService extends BaseService {
} }
if ( if (
authDto.apiKey &&
requestedPermission !== false && requestedPermission !== false &&
!isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions }) currentPermissions &&
!isGranted({ requested: [requestedPermission], current: currentPermissions })
) { ) {
throw new ForbiddenException(`Missing required permission: ${requestedPermission}`); throw new ForbiddenException(`Missing required permission: ${requestedPermission}`);
} }
@ -322,7 +333,7 @@ export class AuthService extends BaseService {
await this.syncProfilePicture(user, profile.picture); await this.syncProfilePicture(user, profile.picture);
} }
return this.createLoginResponse(user, loginDetails); return this.createLoginResponse(user, loginDetails, [Permission.All]);
} }
private async syncProfilePicture(user: UserAdmin, url: string) { private async syncProfilePicture(user: UserAdmin, url: string) {
@ -492,6 +503,7 @@ export class AuthService extends BaseService {
user: session.user, user: session.user,
session: { session: {
id: session.id, id: session.id,
permissions: session.permissions,
hasElevatedPermission, hasElevatedPermission,
}, },
}; };
@ -521,18 +533,31 @@ export class AuthService extends BaseService {
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null }); await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
} }
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { private async createLoginResponse(
user: UserAdmin,
{ deviceOS, deviceType }: LoginDetails,
permissions: Permission[],
): Promise<LoginResponseDto> {
const token = this.cryptoRepository.randomBytesAsText(32); const token = this.cryptoRepository.randomBytesAsText(32);
const tokenHashed = this.cryptoRepository.hashSha256(token); const tokenHashed = this.cryptoRepository.hashSha256(token);
await this.sessionRepository.create({ await this.sessionRepository.create({ token: tokenHashed, deviceOS, deviceType, userId: user.id, permissions });
token: tokenHashed,
deviceOS: loginDetails.deviceOS,
deviceType: loginDetails.deviceType,
userId: user.id,
});
return mapLoginResponse(user, token); const onboardingMetadata = user.metadata.find(
(item): item is UserMetadataItem<UserMetadataKey.Onboarding> => item.key === UserMetadataKey.Onboarding,
)?.value;
return {
accessToken: token,
userId: user.id,
userEmail: user.email,
name: user.name,
isAdmin: user.isAdmin,
profileImagePath: user.profileImagePath,
shouldChangePassword: user.shouldChangePassword,
isOnboarded: onboardingMetadata?.isOnboarded ?? false,
permissions,
};
} }
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T { private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {

View file

@ -31,15 +31,21 @@ export class SessionService extends BaseService {
throw new BadRequestException('This endpoint can only be used with a session token'); throw new BadRequestException('This endpoint can only be used with a session token');
} }
const parent = await this.sessionRepository.get(auth.session.id);
if (!parent) {
throw new BadRequestException('Session not found');
}
const token = this.cryptoRepository.randomBytesAsText(32); const token = this.cryptoRepository.randomBytesAsText(32);
const tokenHashed = this.cryptoRepository.hashSha256(token); const tokenHashed = this.cryptoRepository.hashSha256(token);
const session = await this.sessionRepository.create({ const session = await this.sessionRepository.create({
parentId: auth.session.id, parentId: parent.id,
userId: auth.user.id, userId: auth.user.id,
expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
deviceType: dto.deviceType, deviceType: dto.deviceType,
deviceOS: dto.deviceOS, deviceOS: dto.deviceOS,
token: tokenHashed, token: tokenHashed,
permissions: parent.permissions,
}); });
return { ...mapSession(session), token }; return { ...mapSession(session), token };

View file

@ -135,6 +135,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
userId: newUuid(), userId: newUuid(),
pinExpiresAt: newDate(), pinExpiresAt: newDate(),
isPendingSyncReset: false, isPendingSyncReset: false,
permissions: [Permission.All],
...session, ...session,
}); });