mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
feat(auth): retain user agent, api version and received at on revoked sessions
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class revokedSessionData1661771230400 implements MigrationInterface {
|
||||
name = 'revokedSessionData1661771230400'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `received_at` datetime NULL')
|
||||
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `user_agent` text NULL')
|
||||
await queryRunner.query('ALTER TABLE `revoked_sessions` ADD `api_version` varchar(255) NULL')
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -4,17 +4,23 @@ import { UserDisabledSessionUserAgentLoggingEvent } from '@standardnotes/domain-
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
|
||||
import { UserDisabledSessionUserAgentLoggingEventHandler } from './UserDisabledSessionUserAgentLoggingEventHandler'
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
|
||||
describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
|
||||
let sessionRepository: SessionRepositoryInterface
|
||||
let revokedSessionRepository: RevokedSessionRepositoryInterface
|
||||
let event: UserDisabledSessionUserAgentLoggingEvent
|
||||
|
||||
const createHandler = () => new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository)
|
||||
const createHandler = () =>
|
||||
new UserDisabledSessionUserAgentLoggingEventHandler(sessionRepository, revokedSessionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
|
||||
sessionRepository.clearUserAgentByUserUuid = jest.fn()
|
||||
|
||||
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
|
||||
revokedSessionRepository.clearUserAgentByUserUuid = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
@@ -26,5 +32,6 @@ describe('UserDisabledSessionUserAgentLoggingEventHandler', () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(sessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
|
||||
expect(revokedSessionRepository.clearUserAgentByUserUuid).toHaveBeenCalledWith('1-2-3')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,13 +2,18 @@ import { DomainEventHandlerInterface, UserDisabledSessionUserAgentLoggingEvent }
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class UserDisabledSessionUserAgentLoggingEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface) {}
|
||||
constructor(
|
||||
@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface,
|
||||
@inject(TYPES.SessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: UserDisabledSessionUserAgentLoggingEvent): Promise<void> {
|
||||
await this.sessionRepository.clearUserAgentByUserUuid(event.payload.userUuid)
|
||||
await this.revokedSessionRepository.clearUserAgentByUserUuid(event.payload.userUuid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,25 @@ export class RevokedSession {
|
||||
)
|
||||
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
|
||||
declare user: Promise<User>
|
||||
|
||||
@Column({
|
||||
name: 'received_at',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
})
|
||||
declare receivedAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'user_agent',
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
})
|
||||
declare userAgent: string | null
|
||||
|
||||
@Column({
|
||||
name: 'api_version',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
declare apiVersion: string
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { RevokedSession } from './RevokedSession'
|
||||
|
||||
export interface RevokedSessionRepositoryInterface {
|
||||
@@ -5,4 +6,5 @@ export interface RevokedSessionRepositoryInterface {
|
||||
findAllByUserUuid(userUuid: string): Promise<Array<RevokedSession>>
|
||||
save(revokedSession: RevokedSession): Promise<RevokedSession>
|
||||
remove(revokedSession: RevokedSession): Promise<RevokedSession>
|
||||
clearUserAgentByUserUuid(userUuid: Uuid): Promise<void>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'reflect-metadata'
|
||||
import * as winston from 'winston'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
import { Session } from './Session'
|
||||
import { SessionRepositoryInterface } from './SessionRepositoryInterface'
|
||||
import { SessionService } from './SessionService'
|
||||
@@ -47,6 +48,7 @@ describe('SessionService', () => {
|
||||
session.uuid = '2e1e43'
|
||||
session.userUuid = '1-2-3'
|
||||
session.userAgent = 'Chrome'
|
||||
session.apiVersion = ApiVersion.v20200115
|
||||
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
|
||||
|
||||
@@ -80,6 +82,7 @@ describe('SessionService', () => {
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
|
||||
deviceDetector = {} as jest.Mocked<UAParser>
|
||||
deviceDetector.setUA = jest.fn().mockReturnThis()
|
||||
@@ -111,6 +114,7 @@ describe('SessionService', () => {
|
||||
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
|
||||
uuid: '2e1e43',
|
||||
received: true,
|
||||
receivedAt: new Date(1),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -476,6 +480,8 @@ describe('SessionService', () => {
|
||||
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
|
||||
uuid: '2e1e43',
|
||||
userUuid: '1-2-3',
|
||||
userAgent: 'Chrome',
|
||||
apiVersion: '20200115',
|
||||
createdAt: expect.any(Date),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -203,6 +203,7 @@ export class SessionService implements SessionServiceInterface {
|
||||
|
||||
async markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession> {
|
||||
revokedSession.received = true
|
||||
revokedSession.receivedAt = this.timer.getUTCDate()
|
||||
|
||||
return this.revokedSessionRepository.save(revokedSession)
|
||||
}
|
||||
@@ -224,7 +225,9 @@ export class SessionService implements SessionServiceInterface {
|
||||
const revokedSession = new RevokedSession()
|
||||
revokedSession.uuid = session.uuid
|
||||
revokedSession.userUuid = session.userUuid
|
||||
revokedSession.createdAt = dayjs.utc().toDate()
|
||||
revokedSession.createdAt = this.timer.getUTCDate()
|
||||
revokedSession.apiVersion = session.apiVersion
|
||||
revokedSession.userAgent = session.userAgent
|
||||
|
||||
return this.revokedSessionRepository.save(revokedSession)
|
||||
}
|
||||
@@ -246,8 +249,8 @@ export class SessionService implements SessionServiceInterface {
|
||||
}
|
||||
session.userUuid = dto.user.uuid
|
||||
session.apiVersion = dto.apiVersion
|
||||
session.createdAt = dayjs.utc().toDate()
|
||||
session.updatedAt = dayjs.utc().toDate()
|
||||
session.createdAt = this.timer.getUTCDate()
|
||||
session.updatedAt = this.timer.getUTCDate()
|
||||
session.readonlyAccess = dto.readonlyAccess
|
||||
|
||||
return session
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
|
||||
|
||||
import { RevokedSession } from '../../Domain/Session/RevokedSession'
|
||||
|
||||
@@ -9,12 +9,14 @@ import { MySQLRevokedSessionRepository } from './MySQLRevokedSessionRepository'
|
||||
describe('MySQLRevokedSessionRepository', () => {
|
||||
let ormRepository: Repository<RevokedSession>
|
||||
let queryBuilder: SelectQueryBuilder<RevokedSession>
|
||||
let updateQueryBuilder: UpdateQueryBuilder<RevokedSession>
|
||||
let session: RevokedSession
|
||||
|
||||
const createRepository = () => new MySQLRevokedSessionRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<RevokedSession>>
|
||||
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<RevokedSession>>
|
||||
|
||||
session = {} as jest.Mocked<RevokedSession>
|
||||
|
||||
@@ -36,6 +38,24 @@ describe('MySQLRevokedSessionRepository', () => {
|
||||
expect(ormRepository.remove).toHaveBeenCalledWith(session)
|
||||
})
|
||||
|
||||
it('should clear user agent data on all user sessions', async () => {
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
|
||||
|
||||
updateQueryBuilder.update = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.set = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.where = jest.fn().mockReturnThis()
|
||||
updateQueryBuilder.execute = jest.fn()
|
||||
|
||||
await createRepository().clearUserAgentByUserUuid('1-2-3')
|
||||
|
||||
expect(updateQueryBuilder.update).toHaveBeenCalled()
|
||||
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
|
||||
userAgent: null,
|
||||
})
|
||||
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
|
||||
expect(updateQueryBuilder.execute).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should find one session by id', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(session)
|
||||
|
||||
@@ -20,6 +20,17 @@ export class MySQLRevokedSessionRepository implements RevokedSessionRepositoryIn
|
||||
return this.ormRepository.remove(revokedSession)
|
||||
}
|
||||
|
||||
async clearUserAgentByUserUuid(userUuid: string): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder('revoked_session')
|
||||
.update()
|
||||
.set({
|
||||
userAgent: null,
|
||||
})
|
||||
.where('user_uuid = :userUuid', { userUuid })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async findAllByUserUuid(userUuid: string): Promise<RevokedSession[]> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('revoked_session')
|
||||
|
||||
Reference in New Issue
Block a user