mirror of
https://github.com/immich-app/immich.git
synced 2025-12-26 09:14:58 +03:00
feat: lock auth session (#18322)
This commit is contained in:
@@ -924,13 +924,13 @@ describe(AuthService.name, () => {
|
||||
const user = factory.userAdmin();
|
||||
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||
mocks.session.getByUserId.mockResolvedValue([currentSession]);
|
||||
mocks.session.lockAll.mockResolvedValue(void 0);
|
||||
mocks.session.update.mockResolvedValue(currentSession);
|
||||
|
||||
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
|
||||
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
|
||||
expect(mocks.session.update).toHaveBeenCalledWith(currentSession.id, { pinExpiresAt: null });
|
||||
expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id);
|
||||
});
|
||||
|
||||
it('should throw if the PIN code does not match', async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
PinCodeChangeDto,
|
||||
PinCodeResetDto,
|
||||
PinCodeSetupDto,
|
||||
SessionUnlockDto,
|
||||
SignUpDto,
|
||||
mapLoginResponse,
|
||||
} from 'src/dtos/auth.dto';
|
||||
@@ -123,24 +124,21 @@ export class AuthService extends BaseService {
|
||||
|
||||
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
this.resetPinChecks(user, dto);
|
||||
this.validatePinCode(user, dto);
|
||||
|
||||
await this.userRepository.update(auth.user.id, { pinCode: null });
|
||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||
for (const session of sessions) {
|
||||
await this.sessionRepository.update(session.id, { pinExpiresAt: null });
|
||||
}
|
||||
await this.sessionRepository.lockAll(auth.user.id);
|
||||
}
|
||||
|
||||
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
this.resetPinChecks(user, dto);
|
||||
this.validatePinCode(user, dto);
|
||||
|
||||
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
|
||||
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
||||
}
|
||||
|
||||
private resetPinChecks(
|
||||
private validatePinCode(
|
||||
user: { pinCode: string | null; password: string | null },
|
||||
dto: { pinCode?: string; password?: string },
|
||||
) {
|
||||
@@ -474,23 +472,27 @@ export class AuthService extends BaseService {
|
||||
throw new UnauthorizedException('Invalid user token');
|
||||
}
|
||||
|
||||
async verifyPinCode(auth: AuthDto, dto: PinCodeSetupDto): Promise<void> {
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
this.resetPinChecks(user, { pinCode: dto.pinCode });
|
||||
|
||||
async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise<void> {
|
||||
if (!auth.session) {
|
||||
throw new BadRequestException('Session is missing');
|
||||
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||
}
|
||||
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
this.validatePinCode(user, { pinCode: dto.pinCode });
|
||||
|
||||
await this.sessionRepository.update(auth.session.id, {
|
||||
pinExpiresAt: new Date(DateTime.now().plus({ minutes: 15 }).toJSDate()),
|
||||
pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(),
|
||||
});
|
||||
}
|
||||
|
||||
async lockSession(auth: AuthDto): Promise<void> {
|
||||
if (!auth.session) {
|
||||
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||
}
|
||||
|
||||
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
|
||||
}
|
||||
|
||||
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||
@@ -526,10 +528,14 @@ export class AuthService extends BaseService {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined;
|
||||
|
||||
return {
|
||||
pinCode: !!user.pinCode,
|
||||
password: !!user.password,
|
||||
isElevated: !!auth.session?.hasElevatedPermission,
|
||||
expiresAt: session?.expiresAt?.toISOString(),
|
||||
pinExpiresAt: session?.pinExpiresAt?.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class SessionService extends BaseService {
|
||||
const session = await this.sessionRepository.create({
|
||||
parentId: auth.session.id,
|
||||
userId: auth.user.id,
|
||||
expiredAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
|
||||
expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
|
||||
deviceType: dto.deviceType,
|
||||
deviceOS: dto.deviceOS,
|
||||
token: tokenHashed,
|
||||
@@ -49,6 +49,11 @@ export class SessionService extends BaseService {
|
||||
await this.sessionRepository.delete(id);
|
||||
}
|
||||
|
||||
async lock(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] });
|
||||
await this.sessionRepository.update(id, { pinExpiresAt: null });
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto): Promise<void> {
|
||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||
for (const session of sessions) {
|
||||
|
||||
Reference in New Issue
Block a user