Skip to content

Commit

Permalink
Support server side sessions in ids-api.
Browse files Browse the repository at this point in the history
  • Loading branch information
valurefugl committed Dec 19, 2024
1 parent 72a8ce9 commit ed374de
Show file tree
Hide file tree
Showing 12 changed files with 734 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/services/auth/ids-api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { NotificationsModule } from './notifications/notifications.module'
import { PasskeysModule } from './passkeys/passkeys.module'
import { PermissionsModule } from './permissions/permissions.module'
import { ResourcesModule } from './resources/resources.module'
import { SessionsModule } from './sessions/sessions.module'
import { TranslationModule } from './translation/translation.module'
import { UserProfileModule } from './user-profile/user-profile.module'
import { UsersModule } from './users/users.module'
Expand All @@ -56,6 +57,7 @@ import { UsersModule } from './users/users.module'
NotificationsModule,
LoginRestrictionsModule,
PasskeysModule,
SessionsModule,
ConfigModule.forRoot({
isGlobal: true,
load: [
Expand Down
104 changes: 104 additions & 0 deletions apps/services/auth/ids-api/src/app/sessions/sessions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
UseGuards,
VERSION_NEUTRAL,
} from '@nestjs/common'
import { ApiNoContentResponse, ApiTags } from '@nestjs/swagger'

import {
SessionDto,
SessionFilter,
SessionRemoveOptions,
SessionsService,
} from '@island.is/auth-api-lib'
import { IdsAuthGuard, Scopes, ScopesGuard } from '@island.is/auth-nest-tools'
import { NoContentException } from '@island.is/nest/problem'
import { Documentation } from '@island.is/nest/swagger'

@ApiTags('sessions')
@Controller({
path: 'sessions',
version: ['1', VERSION_NEUTRAL],
})
@UseGuards(IdsAuthGuard, ScopesGuard)
@Scopes('@identityserver.api/authentication')
export class SessionsController {
constructor(private readonly sessionsService: SessionsService) {}

@Get()
@Documentation({
description: 'Retrieves multiple sessions based on the provided filter.',
response: { status: 200, type: [SessionDto] },
})
findMany(@Query() filter: SessionFilter): Promise<SessionDto[]> {
return this.sessionsService.findMany(filter)
}

@Get(':key')
@Documentation({
description: 'Retrieves a single session by its key.',
response: { status: 200, type: SessionDto },
})
@ApiNoContentResponse()
async findOne(@Param('key') key: string): Promise<SessionDto> {
const result = await this.sessionsService.findOne(key)
if (result == null) {
throw new NoContentException()
}
return result
}

@Post()
@Documentation({
description: 'Creates a new session.',
response: { status: 201, type: SessionDto },
})
create(@Body() session: SessionDto): Promise<SessionDto | null> {
return this.sessionsService.create(session)
}

@Put()
@Documentation({
description: 'Updates an existing session.',
response: { status: 200, type: SessionDto },
})
update(@Body() session: SessionDto): Promise<SessionDto | null> {
return this.sessionsService.update(session)
}

@Delete(':key')
@Documentation({
description: 'Deletes a session by its key.',
response: { status: 204 },
})
delete(@Param('key') key: string): Promise<void> {
return this.sessionsService.delete(key)
}

@Post('delete')
@Documentation({
description: 'Deletes multiple sessions based on the provided filter.',
response: { status: 204 },
})
deleteMany(@Body() filter: SessionFilter): Promise<void> {
return this.sessionsService.deleteMany(filter)
}

@Post('delete-expired')
@Documentation({
description: 'Retrieves and removes expired sessions.',
response: { status: 200, type: [SessionDto] },
})
getAndRemoveExpired(
@Body() options: SessionRemoveOptions,
): Promise<SessionDto[]> {
return this.sessionsService.getAndRemoveExpired(options)
}
}
11 changes: 11 additions & 0 deletions apps/services/auth/ids-api/src/app/sessions/sessions.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'

import { SessionsModule as AuthSessionsModule } from '@island.is/auth-api-lib'

import { SessionsController } from './sessions.controller'

@Module({
imports: [AuthSessionsModule],
controllers: [SessionsController],
})
export class SessionsModule {}
78 changes: 78 additions & 0 deletions libs/auth-api-lib/migrations/20241216160603-create-sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict'

module.exports = {
async up(queryInterface, Sequelize) {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.createTable(
'session',
{
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false,
},
scheme: {
type: Sequelize.STRING,
allowNull: false,
},
subject_id: {
type: Sequelize.STRING,
allowNull: false,
},
session_id: {
type: Sequelize.STRING,
},
created: {
type: Sequelize.DATE,
allowNull: false,
},
renewed: {
type: Sequelize.DATE,
allowNull: false,
},
expires: {
type: Sequelize.DATE,
},
data: {
type: Sequelize.TEXT,
allowNull: false,
},
actor_subject_id: {
type: Sequelize.STRING,
allowNull: false,
},
modified: {
type: Sequelize.DATE,
},
},
{ transaction },
)

await queryInterface.addIndex('session', ['subject_id'], {
name: 'session_subject_id_idx',
transaction,
})

await queryInterface.addIndex('session', ['session_id'], {
name: 'session_session_id_idx',
transaction,
})

await queryInterface.addIndex('session', ['actor_subject_id'], {
name: 'session_actor_subject_id_idx',
transaction,
})

await queryInterface.addIndex('session', ['expires'], {
name: 'session_expires_idx',
transaction,
})
})
},

async down(queryInterface) {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.dropTable('session', { transaction })
})
},
}
7 changes: 7 additions & 0 deletions libs/auth-api-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,10 @@ export { isUnderXAge } from './lib/delegations/utils/isUnderXAge'
export * from './lib/passkeys-core/passkeys-core.module'
export * from './lib/passkeys-core/passkeys-core.service'
export * from './lib/passkeys-core/passkeys-core.config'

// Sessions module
export * from './lib/sessions/sessions.module'
export * from './lib/sessions/sessions.service'
export * from './lib/sessions/dto/session-filter.dto'
export * from './lib/sessions/dto/session-remove-options.dto'
export * from './lib/sessions/dto/session.dto'
19 changes: 19 additions & 0 deletions libs/auth-api-lib/src/lib/sessions/dto/session-filter.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'

export class SessionFilter {
@IsString()
@IsOptional()
@ApiPropertyOptional()
subjectId?: string

@IsString()
@IsOptional()
@ApiPropertyOptional()
sessionId?: string

@IsString()
@IsOptional()
@ApiPropertyOptional()
actorSubjectId?: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNumber, IsOptional } from 'class-validator'

export class SessionRemoveOptions {
@IsNumber()
@IsOptional()
@ApiProperty()
count!: number
}
45 changes: 45 additions & 0 deletions libs/auth-api-lib/src/lib/sessions/dto/session.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsDate, IsOptional, IsString } from 'class-validator'

export class SessionDto {
@IsString()
@ApiProperty()
key!: string

@IsString()
@ApiProperty()
scheme!: string

@IsString()
@ApiProperty()
subjectId!: string

@IsString()
@ApiPropertyOptional()
sessionId?: string

@IsDate()
@Type(() => Date)
@ApiProperty()
created!: Date

@IsDate()
@Type(() => Date)
@ApiProperty()
renewed!: Date

@IsDate()
@Type(() => Date)
@IsOptional()
@ApiPropertyOptional()
expires?: Date

@IsString()
@ApiProperty()
ticket!: string

@IsString()
@ApiProperty()
actorSubjectId!: string
}
82 changes: 82 additions & 0 deletions libs/auth-api-lib/src/lib/sessions/models/session.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Column,
DataType,
Model,
PrimaryKey,
Table,
} from 'sequelize-typescript'

import { SessionDto } from '../dto/session.dto'

@Table({
tableName: 'session',
})
export class Session extends Model {
@PrimaryKey
@Column({
type: DataType.STRING,
primaryKey: true,
allowNull: false,
})
key!: string

@Column({
type: DataType.STRING,
allowNull: false,
})
scheme!: string

@Column({
type: DataType.STRING,
allowNull: false,
})
subjectId!: string

@Column({
type: DataType.STRING,
})
sessionId?: string

@Column({
type: DataType.DATE,
allowNull: false,
})
created!: Date

@Column({
type: DataType.DATE,
allowNull: false,
})
renewed!: Date

@Column({
type: DataType.DATE,
})
expires?: Date

@Column({
type: DataType.TEXT,
allowNull: false,
})
data!: string

@Column({
type: DataType.STRING,
allowNull: false,
})
actorSubjectId!: string

toDto(): SessionDto {
return {
key: this.key,
scheme: this.scheme,
subjectId: this.subjectId,
sessionId: this.sessionId,
created: this.created,
renewed: this.renewed,
expires: this.expires,
ticket: this.data,
actorSubjectId: this.actorSubjectId,
}
}
}
12 changes: 12 additions & 0 deletions libs/auth-api-lib/src/lib/sessions/sessions.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { SequelizeModule } from '@nestjs/sequelize'

import { Session } from './models/session.model'
import { SessionsService } from './sessions.service'

@Module({
imports: [SequelizeModule.forFeature([Session])],
providers: [SessionsService],
exports: [SessionsService],
})
export class SessionsModule {}
Loading

0 comments on commit ed374de

Please sign in to comment.