feat(web,server): manage authorized devices (#2329)

* feat: manage authorized devices

* chore: open api

* get header from mobile app

* write header from mobile app

* styling

* fix unit test

* feat: use relative time

* feat: update access time

* fix: tests

* chore: confirm wording

* chore: bump test coverage thresholds

* feat: add some icons

* chore: icon tweaks

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2023-04-25 22:19:23 -04:00 committed by GitHub
parent aa91b946fa
commit b8313abfa8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1209 additions and 93 deletions

View file

@ -1,10 +1,17 @@
import { SystemConfig, UserEntity } from '@app/infra/entities';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { ISystemConfigRepository } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { LoginResponseDto, mapLoginResponse } from './response-dto';
import { IUserTokenRepository, UserTokenCore } from '../user-token';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
import { LoginResponseDto, mapLoginResponse } from './response-dto';
export interface LoginDetails {
isSecure: boolean;
clientIp: string;
deviceType: string;
deviceOS: string;
}
export class AuthCore {
private userTokenCore: UserTokenCore;
@ -23,7 +30,7 @@ export class AuthCore {
return this.config.passwordLogin.enabled;
}
public getCookies(loginResponse: LoginResponseDto, authType: AuthType, isSecure: boolean) {
getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) {
const maxAge = 400 * 24 * 3600; // 400 days
let authTypeCookie = '';
@ -39,10 +46,10 @@ export class AuthCore {
return [accessTokenCookie, authTypeCookie];
}
public async createLoginResponse(user: UserEntity, authType: AuthType, isSecure: boolean) {
const accessToken = await this.userTokenCore.createToken(user);
async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) {
const accessToken = await this.userTokenCore.create(user, loginDetails);
const response = mapLoginResponse(user, accessToken);
const cookie = this.getCookies(response, authType, isSecure);
const cookie = this.getCookies(response, authType, loginDetails);
return { response, cookie };
}

View file

@ -32,6 +32,12 @@ import { AuthUserDto, SignUpDto } from './dto';
const email = 'test@immich.com';
const sub = 'my-auth-user-sub';
const loginDetails = {
isSecure: true,
clientIp: '127.0.0.1',
deviceOS: '',
deviceType: '',
};
const fixtures = {
login: {
@ -40,8 +46,6 @@ const fixtures = {
},
};
const CLIENT_IP = '127.0.0.1';
describe('AuthService', () => {
let sut: AuthService;
let cryptoMock: jest.Mocked<ICryptoRepository>;
@ -96,32 +100,39 @@ describe('AuthService', () => {
it('should throw an error if password login is disabled', async () => {
sut = create(systemConfigStub.disabled);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).rejects.toBeInstanceOf(UnauthorizedException);
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should check the user exists', async () => {
userMock.getByEmail.mockResolvedValue(null);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
it('should check the user has a password', async () => {
userMock.getByEmail.mockResolvedValue({} as UserEntity);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
it('should successfully log the user in', async () => {
userMock.getByEmail.mockResolvedValue(userEntityStub.user1);
userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).resolves.toEqual(loginResponseStub.user1password);
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
it('should generate the cookie headers (insecure)', async () => {
userMock.getByEmail.mockResolvedValue(userEntityStub.user1);
userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
await expect(sut.login(fixtures.login, CLIENT_IP, false)).resolves.toEqual(loginResponseStub.user1insecure);
await expect(
sut.login(fixtures.login, {
clientIp: '127.0.0.1',
isSecure: false,
deviceOS: '',
deviceType: '',
}),
).resolves.toEqual(loginResponseStub.user1insecure);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
});
@ -205,7 +216,7 @@ describe('AuthService', () => {
redirectUri: '/auth/login?autoLaunch=0',
});
expect(userTokenMock.delete).toHaveBeenCalledWith('token123');
expect(userTokenMock.delete).toHaveBeenCalledWith('123', 'token123');
});
});
@ -240,7 +251,7 @@ describe('AuthService', () => {
it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(userEntityStub.user1);
userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken);
userTokenMock.getByToken.mockResolvedValue(userTokenEntityStub.userToken);
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual(userEntityStub.user1);
});
@ -276,16 +287,32 @@ describe('AuthService', () => {
describe('validate - user token', () => {
it('should throw if no token is found', async () => {
userTokenMock.get.mockResolvedValue(null);
userTokenMock.getByToken.mockResolvedValue(null);
const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should return an auth dto', async () => {
userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken);
userTokenMock.getByToken.mockResolvedValue(userTokenEntityStub.userToken);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(userEntityStub.user1);
});
it('should update when access time exceeds an hour', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenEntityStub.inactiveToken);
userTokenMock.save.mockResolvedValue(userTokenEntityStub.userToken);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual(userEntityStub.user1);
expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({
id: 'not_active',
token: 'auth_token',
userId: 'immich_id',
createdAt: new Date('2021-01-01'),
updatedAt: expect.any(Date),
deviceOS: 'Android',
deviceType: 'Mobile',
});
});
});
describe('validate - api key', () => {
@ -303,4 +330,38 @@ describe('AuthService', () => {
expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)');
});
});
describe('getDevices', () => {
it('should get the devices', async () => {
userTokenMock.getAll.mockResolvedValue([userTokenEntityStub.userToken, userTokenEntityStub.inactiveToken]);
await expect(sut.getDevices(authStub.user1)).resolves.toEqual([
{
createdAt: '2021-01-01T00:00:00.000Z',
current: true,
deviceOS: '',
deviceType: '',
id: 'token-id',
updatedAt: expect.any(String),
},
{
createdAt: '2021-01-01T00:00:00.000Z',
current: false,
deviceOS: 'Android',
deviceType: 'Mobile',
id: 'not_active',
updatedAt: expect.any(String),
},
]);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.id);
});
});
describe('logoutDevice', () => {
it('should logout the device', async () => {
await sut.logoutDevice(authStub.user1, 'token-1');
expect(userTokenMock.delete).toHaveBeenCalledWith(authStub.user1.id, 'token-1');
});
});
});

View file

@ -12,7 +12,7 @@ import { OAuthCore } from '../oauth/oauth.core';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore } from '../user';
import { AuthType, IMMICH_ACCESS_COOKIE } from './auth.constant';
import { AuthCore } from './auth.core';
import { AuthCore, LoginDetails } from './auth.core';
import { ICryptoRepository } from '../crypto/crypto.repository';
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto';
import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
@ -21,6 +21,7 @@ import cookieParser from 'cookie';
import { ISharedLinkRepository, ShareCore } from '../share';
import { APIKeyCore } from '../api-key/api-key.core';
import { IKeyRepository } from '../api-key';
import { AuthDeviceResponseDto, mapUserToken } from './response-dto';
@Injectable()
export class AuthService {
@ -53,8 +54,7 @@ export class AuthService {
public async login(
loginCredential: LoginCredentialDto,
clientIp: string,
isSecure: boolean,
loginDetails: LoginDetails,
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
if (!this.authCore.isPasswordLoginEnabled()) {
throw new UnauthorizedException('Password login has been disabled');
@ -69,16 +69,18 @@ export class AuthService {
}
if (!user) {
this.logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`);
this.logger.warn(
`Failed login attempt for user ${loginCredential.email} from ip address ${loginDetails.clientIp}`,
);
throw new BadRequestException('Incorrect email or password');
}
return this.authCore.createLoginResponse(user, AuthType.PASSWORD, isSecure);
return this.authCore.createLoginResponse(user, AuthType.PASSWORD, loginDetails);
}
public async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> {
if (authUser.accessTokenId) {
await this.userTokenCore.deleteToken(authUser.accessTokenId);
await this.userTokenCore.delete(authUser.id, authUser.accessTokenId);
}
if (authType === AuthType.OAUTH) {
@ -152,6 +154,15 @@ export class AuthService {
throw new UnauthorizedException('Authentication required');
}
async getDevices(authUser: AuthUserDto): Promise<AuthDeviceResponseDto[]> {
const userTokens = await this.userTokenCore.getAll(authUser.id);
return userTokens.map((userToken) => mapUserToken(userToken, authUser.accessTokenId));
}
async logoutDevice(authUser: AuthUserDto, deviceId: string): Promise<void> {
await this.userTokenCore.delete(authUser.id, deviceId);
}
private getBearerToken(headers: IncomingHttpHeaders): string | null {
const [type, token] = (headers.authorization || '').split(' ');
if (type.toLowerCase() === 'bearer') {

View file

@ -1,4 +1,5 @@
export * from './auth.constant';
export * from './auth.core';
export * from './auth.service';
export * from './dto';
export * from './response-dto';

View file

@ -0,0 +1,19 @@
import { UserTokenEntity } from '@app/infra/entities';
export class AuthDeviceResponseDto {
id!: string;
createdAt!: string;
updatedAt!: string;
current!: boolean;
deviceType!: string;
deviceOS!: string;
}
export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
current: currentId === entity.id,
deviceOS: entity.deviceOS,
deviceType: entity.deviceType,
});

View file

@ -1,4 +1,5 @@
export * from './admin-signup-response.dto';
export * from './auth-device-response.dto';
export * from './login-response.dto';
export * from './logout-response.dto';
export * from './validate-asset-token-response.dto';

View file

@ -1,13 +1,4 @@
import { ApiResponseProperty } from '@nestjs/swagger';
export class LogoutResponseDto {
constructor(successful: boolean) {
this.successful = successful;
}
@ApiResponseProperty()
successful!: boolean;
@ApiResponseProperty()
redirectUri!: string;
}

View file

@ -1,10 +1,3 @@
import { ApiProperty } from '@nestjs/swagger';
export class ValidateAccessTokenResponseDto {
constructor(authStatus: boolean) {
this.authStatus = authStatus;
}
@ApiProperty({ type: 'boolean' })
authStatus!: boolean;
}

View file

@ -17,9 +17,16 @@ import { ISystemConfigRepository } from '../system-config';
import { IUserRepository } from '../user';
import { IUserTokenRepository } from '../user-token';
import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock';
import { LoginDetails } from '../auth';
const email = 'user@immich.com';
const sub = 'my-auth-user-sub';
const loginDetails: LoginDetails = {
isSecure: true,
clientIp: '127.0.0.1',
deviceOS: '',
deviceType: '',
};
describe('OAuthService', () => {
let sut: OAuthService;
@ -95,13 +102,13 @@ describe('OAuthService', () => {
describe('login', () => {
it('should throw an error if OAuth is not enabled', async () => {
await expect(sut.login({ url: '' }, true)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.login({ url: '' }, loginDetails)).rejects.toBeInstanceOf(BadRequestException);
});
it('should not allow auto registering', async () => {
sut = create(systemConfigStub.noAutoRegister);
userMock.getByEmail.mockResolvedValue(null);
await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, true)).rejects.toBeInstanceOf(
await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
@ -113,7 +120,7 @@ describe('OAuthService', () => {
userMock.update.mockResolvedValue(userEntityStub.user1);
userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, true)).resolves.toEqual(
await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
loginResponseStub.user1oauth,
);
@ -129,7 +136,7 @@ describe('OAuthService', () => {
userMock.create.mockResolvedValue(userEntityStub.user1);
userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, true)).resolves.toEqual(
await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
loginResponseStub.user1oauth,
);
@ -143,7 +150,7 @@ describe('OAuthService', () => {
userMock.getByOAuthId.mockResolvedValue(userEntityStub.user1);
userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
await sut.login({ url: `app.immich:/?code=abc123` }, true);
await sut.login({ url: `app.immich:/?code=abc123` }, loginDetails);
expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' });
});

View file

@ -1,7 +1,7 @@
import { SystemConfig } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
import { AuthType, AuthUserDto, LoginResponseDto } from '../auth';
import { AuthCore } from '../auth/auth.core';
import { AuthCore, LoginDetails } from '../auth/auth.core';
import { ICryptoRepository } from '../crypto';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore, UserResponseDto } from '../user';
@ -39,7 +39,10 @@ export class OAuthService {
return this.oauthCore.generateConfig(dto);
}
async login(dto: OAuthCallbackDto, isSecure: boolean): Promise<{ response: LoginResponseDto; cookie: string[] }> {
async login(
dto: OAuthCallbackDto,
loginDetails: LoginDetails,
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
const profile = await this.oauthCore.callback(dto.url);
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
@ -66,7 +69,7 @@ export class OAuthService {
user = await this.userCore.createUser(this.oauthCore.asUser(profile));
}
return this.authCore.createLoginResponse(user, AuthType.OAUTH, isSecure);
return this.authCore.createLoginResponse(user, AuthType.OAUTH, loginDetails);
}
public async link(user: AuthUserDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {

View file

@ -1,5 +1,7 @@
import { UserEntity } from '@app/infra/entities';
import { UserEntity, UserTokenEntity } from '@app/infra/entities';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { LoginDetails } from '../auth';
import { ICryptoRepository } from '../crypto';
import { IUserTokenRepository } from './user-token.repository';
@ -9,9 +11,16 @@ export class UserTokenCore {
async validate(tokenValue: string) {
const hashedToken = this.crypto.hashSha256(tokenValue);
const token = await this.repository.get(hashedToken);
let token = await this.repository.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.repository.save({ ...token, updatedAt: new Date() });
}
return {
...token.user,
isPublicUser: false,
@ -25,18 +34,24 @@ export class UserTokenCore {
throw new UnauthorizedException('Invalid user token');
}
public async createToken(user: UserEntity): Promise<string> {
async create(user: UserEntity, loginDetails: LoginDetails): Promise<string> {
const key = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
const token = this.crypto.hashSha256(key);
await this.repository.create({
token,
user,
deviceOS: loginDetails.deviceOS,
deviceType: loginDetails.deviceType,
});
return key;
}
public async deleteToken(id: string): Promise<void> {
await this.repository.delete(id);
async delete(userId: string, id: string): Promise<void> {
await this.repository.delete(userId, id);
}
getAll(userId: string): Promise<UserTokenEntity[]> {
return this.repository.getAll(userId);
}
}

View file

@ -4,7 +4,9 @@ export const IUserTokenRepository = 'IUserTokenRepository';
export interface IUserTokenRepository {
create(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
delete(userToken: string): Promise<void>;
save(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
delete(userId: string, id: string): Promise<void>;
deleteAll(userId: string): Promise<void>;
get(userToken: string): Promise<UserTokenEntity | null>;
getByToken(token: string): Promise<UserTokenEntity | null>;
getAll(userId: string): Promise<UserTokenEntity[]>;
}

View file

@ -391,9 +391,22 @@ export const userTokenEntityStub = {
userToken: Object.freeze<UserTokenEntity>({
id: 'token-id',
token: 'auth_token',
userId: userEntityStub.user1.id,
user: userEntityStub.user1,
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
createdAt: new Date('2021-01-01'),
updatedAt: new Date(),
deviceType: '',
deviceOS: '',
}),
inactiveToken: Object.freeze<UserTokenEntity>({
id: 'not_active',
token: 'auth_token',
userId: userEntityStub.user1.id,
user: userEntityStub.user1,
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
deviceType: 'Mobile',
deviceOS: 'Android',
}),
};

View file

@ -3,8 +3,10 @@ import { IUserTokenRepository } from '../src';
export const newUserTokenRepositoryMock = (): jest.Mocked<IUserTokenRepository> => {
return {
create: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
deleteAll: jest.fn(),
get: jest.fn(),
getByToken: jest.fn(),
getAll: jest.fn(),
};
};

View file

@ -9,12 +9,21 @@ export class UserTokenEntity {
@Column({ select: false })
token!: string;
@Column()
userId!: string;
@ManyToOne(() => UserEntity)
user!: UserEntity;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: string;
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: string;
updatedAt!: Date;
@Column({ default: '' })
deviceType!: string;
@Column({ default: '' })
deviceOS!: string;
}

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class FixNullableRelations1682371561743 implements MigrationInterface {
name = 'FixNullableRelations1682371561743';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_token" DROP CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918"`);
await queryRunner.query(`ALTER TABLE "user_token" ALTER COLUMN "userId" SET NOT NULL`);
await queryRunner.query(
`ALTER TABLE "user_token" ADD CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_token" DROP CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918"`);
await queryRunner.query(`ALTER TABLE "user_token" ALTER COLUMN "userId" DROP NOT NULL`);
await queryRunner.query(
`ALTER TABLE "user_token" ADD CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
}

View file

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddDeviceInfoToUserToken1682371791038 implements MigrationInterface {
name = 'AddDeviceInfoToUserToken1682371791038'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_token" ADD "deviceType" character varying NOT NULL DEFAULT ''`);
await queryRunner.query(`ALTER TABLE "user_token" ADD "deviceOS" character varying NOT NULL DEFAULT ''`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_token" DROP COLUMN "deviceOS"`);
await queryRunner.query(`ALTER TABLE "user_token" DROP COLUMN "deviceType"`);
}
}

View file

@ -6,24 +6,40 @@ import { IUserTokenRepository } from '@app/domain/user-token';
@Injectable()
export class UserTokenRepository implements IUserTokenRepository {
constructor(
@InjectRepository(UserTokenEntity)
private userTokenRepository: Repository<UserTokenEntity>,
) {}
constructor(@InjectRepository(UserTokenEntity) private repository: Repository<UserTokenEntity>) {}
async get(userToken: string): Promise<UserTokenEntity | null> {
return this.userTokenRepository.findOne({ where: { token: userToken }, relations: { user: true } });
getByToken(token: string): Promise<UserTokenEntity | null> {
return this.repository.findOne({ where: { token }, relations: { user: true } });
}
async create(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
return this.userTokenRepository.save(userToken);
getAll(userId: string): Promise<UserTokenEntity[]> {
return this.repository.find({
where: {
userId,
},
relations: {
user: true,
},
order: {
updatedAt: 'desc',
createdAt: 'desc',
},
});
}
async delete(id: string): Promise<void> {
await this.userTokenRepository.delete(id);
create(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
return this.repository.save(userToken);
}
save(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
return this.repository.save(userToken);
}
async delete(userId: string, id: string): Promise<void> {
await this.repository.delete({ userId, id });
}
async deleteAll(userId: string): Promise<void> {
await this.userTokenRepository.delete({ user: { id: userId } });
await this.repository.delete({ userId });
}
}