Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/end session #305

Merged
merged 4 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/api/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server'
import fetchFromApi from "../../../utilities/fetchFromApi";
import getReqAuthToken from "../../../utilities/getReqAuthToken";

const backendUrl = process.env.BACKEND_URL

export async function POST(req: Request) {
try {
const token = getReqAuthToken(req)
const { status } = await fetchFromApi(`${backendUrl}/logout`, token, {
method: 'POST'
})

const response = NextResponse.json(true, { status })

response.cookies.delete('session-token')

return response

} catch (error: any) {
let message = error?.response?.data?.message

if (!message) {
message = 'authPrompt.unableToEndSession'
}

console.log(error)
return NextResponse.json({ error: message }, { status: 500 })
}
}
53 changes: 39 additions & 14 deletions app/dashboard/settings/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
'use client'

import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import axios from "axios";
import {useRouter} from "next/navigation";
import React, {FC, useState} from 'react'
import {useTranslation} from 'react-i18next'
import LighthouseSvg from '../../../src/assets/images/lighthouse-black.svg'
import AppDescription from '../../../src/components/AppDescription/AppDescription'
import AppVersion from '../../../src/components/AppVersion/AppVersion'
import Button, {ButtonFace} from "../../../src/components/Button/Button";
import DashboardWrapper from '../../../src/components/DashboardWrapper/DashboardWrapper'
import Input from '../../../src/components/Input/Input'
import SocialIcon from '../../../src/components/SocialIcon/SocialIcon'
import Toggle from '../../../src/components/Toggle/Toggle'
import Typography from '../../../src/components/Typography/Typography'
import UiModeIcon from '../../../src/components/UiModeIcon/UiModeIcon'
import {
DiscordUrl,
LighthouseBookUrl,
SigPGithubUrl,
SigPIoUrl,
SigPTwitter,
} from '../../../src/constants/constants'
import { UiMode } from '../../../src/constants/enums'
import {DiscordUrl, LighthouseBookUrl, SigPGithubUrl, SigPIoUrl, SigPTwitter,} from '../../../src/constants/constants'
import {UiMode} from '../../../src/constants/enums'
import useLocalStorage from '../../../src/hooks/useLocalStorage'
import useNetworkMonitor from '../../../src/hooks/useNetworkMonitor'
import useSWRPolling from '../../../src/hooks/useSWRPolling'
import useUiMode from '../../../src/hooks/useUiMode'
import { ActivityResponse, OptionalString } from '../../../src/types'
import { BeaconNodeSpecResults, SyncData } from '../../../src/types/beacon'
import { Diagnostics } from '../../../src/types/diagnostic'
import { UsernameStorage } from '../../../src/types/storage'
import {ActivityResponse, OptionalString, ToastType} from '../../../src/types'
import {BeaconNodeSpecResults, SyncData} from '../../../src/types/beacon'
import {Diagnostics} from '../../../src/types/diagnostic'
import {UsernameStorage} from '../../../src/types/storage'
import addClassString from '../../../utilities/addClassString'
import displayToast from "../../../utilities/displayToast";

export interface MainProps {
initNodeHealth: Diagnostics
Expand All @@ -49,7 +47,9 @@ const Main: FC<MainProps> = (props) => {
initActivityData,
} = props

const router = useRouter()
const { SECONDS_PER_SLOT } = beaconSpec
const [isLoading, setIsLoading] = useState(false)
const { isValidatorError, isBeaconError } = useNetworkMonitor()
const { mode, toggleUiMode } = useUiMode()
const [userNameError, setError] = useState<OptionalString>()
Expand All @@ -66,6 +66,28 @@ const Main: FC<MainProps> = (props) => {
storeUserName(value)
}

const handleError = () => {
displayToast(t('authPrompt.unexpectedErrorLogout'), ToastType.ERROR)

}

const logout = async () => {
try {
setIsLoading(true)
const { status } = await axios.post('/api/logout' )

if(status === 200) {
router.push('/')
return
}
handleError()
} catch (e) {
handleError()
} finally {
setIsLoading(false)
}
}

const networkError = isValidatorError || isBeaconError
const slotInterval = SECONDS_PER_SLOT * 1000
const { data: nodeHealth } = useSWRPolling<Diagnostics>('/api/node-health', {
Expand Down Expand Up @@ -193,6 +215,9 @@ const Main: FC<MainProps> = (props) => {
/>
</div>
</div>
<div className="pt-8">
<Button isLoading={isLoading} onClick={logout} type={ButtonFace.ERROR}>{t('endSession')}</Button>
</div>
</div>
</div>
</DashboardWrapper>
Expand Down
1 change: 1 addition & 0 deletions app/error/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import '../../src/i18n'
import { useTranslation } from 'react-i18next'
import Button, { ButtonFace } from '../../src/components/Button/Button'
import Typography from '../../src/components/Typography/Typography'
Expand Down
3 changes: 2 additions & 1 deletion backend/src/activity/activity.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { ActivityController } from './activity.controller';
import { ActivityService } from './activity.service';
import { SequelizeModule } from '@nestjs/sequelize';
import { Activity } from './entities/activity.entity';
import {AuthModule} from "../auth.module";

@Module({
imports: [SequelizeModule.forFeature([Activity])],
imports: [SequelizeModule.forFeature([Activity]), AuthModule],
controllers: [ActivityController],
providers: [ActivityService],
exports: [ActivityService],
Expand Down
2 changes: 2 additions & 0 deletions backend/src/activity/tests/activity.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { HttpService } from '@nestjs/axios';
import { Sequelize } from 'sequelize-typescript';
import { JwtModule } from '@nestjs/jwt';
import { ActivityType } from '../../../../src/types';
import {AuthModule} from "../../auth.module";

describe('ActivityController', () => {
const baseRowData = {
Expand Down Expand Up @@ -58,6 +59,7 @@ describe('ActivityController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
AuthModule,
SequelizeModule.forFeature([Activity]),
SequelizeModule.forRoot({
dialect: 'sqlite',
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { JwtModule } from '@nestjs/jwt';
import { UnauthorizedException } from '@nestjs/common';
import {CacheModule} from "@nestjs/cache-manager";

describe('AppController', () => {
let appController: AppController;
Expand All @@ -11,6 +12,7 @@ describe('AppController', () => {
jest.resetModules();
const app: TestingModule = await Test.createTestingModule({
imports: [
CacheModule.register(),
JwtModule.register({
global: true,
secret: 'fake-value',
Expand Down
14 changes: 13 additions & 1 deletion backend/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import {Body, Controller, HttpCode, HttpStatus, Post, UseGuards, Req} from '@nestjs/common';
import { AppService } from './app.service';
import {SessionGuard} from "./session.guard";

@Controller()
export class AppController {
Expand All @@ -10,4 +11,15 @@ export class AppController {
authenticate(@Body() data: Record<string, any>) {
return this.appService.authenticateSessionPassword(data.password);
}

@Post('/logout')
@UseGuards(SessionGuard)
async logoutSession(@Req() req: Request) {
const authHeader = req.headers['authorization'];
if (!authHeader) {
throw new Error('No Authorization header');
}
const token = authHeader.split(' ')[1];
return await this.appService.invalidateToken(token);
}
}
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TasksModule } from './tasks/tasks.module';
import { JwtModule } from '@nestjs/jwt';
import { GracefulShutdownModule } from 'nestjs-graceful-shutdown';
import { ActivityModule } from './activity/activity.module';
import {CacheModule} from "@nestjs/cache-manager";

@Module({
imports: [
Expand All @@ -26,6 +27,7 @@ import { ActivityModule } from './activity/activity.module';
secret: process.env.API_TOKEN,
signOptions: { expiresIn: '7200s' }, //set to 2 hours
}),
CacheModule.register(),
ScheduleModule.forRoot(),
LogsModule,
TasksModule,
Expand Down
41 changes: 38 additions & 3 deletions backend/src/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import {Inject, Injectable, UnauthorizedException} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import {CACHE_MANAGER} from "@nestjs/cache-manager";
import {Cache} from "cache-manager";
import { v4 as uuidV4 } from 'uuid'

@Injectable()
export class AppService {
constructor(private jwtService: JwtService) {}
constructor(
private jwtService: JwtService,
@Inject(CACHE_MANAGER)
private cacheManager: Cache
) {}
private sessionPassword = process.env.SESSION_PASSWORD;

async invalidateToken(token: string) {
const decoded = this.jwtService.decode(token) as any;
if (!decoded || !decoded.exp || !decoded.jti) {
throw new UnauthorizedException('Invalid token');
}

const expiresIn = decoded.exp * 1000 - Date.now();

if (expiresIn <= 0) {
return { message: 'Token is already expired' };
}

await this.cacheManager.set(`blacklist:${decoded.jti}`, 'blacklisted', expiresIn);

return { message: 'Token invalidated' };
}

async isTokenBlacklisted(token: string): Promise<boolean> {
const decoded = this.jwtService.decode(token) as any;
if (!decoded || !decoded.jti) {
return false;
}

const result = await this.cacheManager.get(`blacklist:${decoded.jti}`);
return result === 'blacklisted';
}

async authenticateSessionPassword(password: string) {
if (!this.sessionPassword) {
throw new Error('authPrompt.noPasswordFound');
Expand All @@ -15,7 +49,8 @@ export class AppService {
throw new UnauthorizedException('authPrompt.invalidPassword');
}

const payload = { sub: 'authenticated_session' };
const jti = uuidV4().toString();
const payload = { sub: 'authenticated_session', jti };

return {
access_token: await this.jwtService.signAsync(payload),
Expand Down
17 changes: 17 additions & 0 deletions backend/src/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { AppService } from './app.service';
import { JwtModule } from '@nestjs/jwt';
import { CacheModule } from '@nestjs/cache-manager';

@Module({
imports: [
JwtModule.register({
secret: process.env.API_TOKEN,
signOptions: { expiresIn: '7200s' },
}),
CacheModule.register(),
],
providers: [AppService],
exports: [AppService],
})
export class AuthModule {}
3 changes: 2 additions & 1 deletion backend/src/beacon/beacon.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { BeaconService } from './beacon.service';
import { BeaconController } from './beacon.controller';
import { UtilsModule } from '../utils/utils.module';
import { CacheModule } from '@nestjs/cache-manager';
import {AuthModule} from "../auth.module";

@Module({
imports: [UtilsModule, CacheModule.register()],
imports: [UtilsModule, CacheModule.register(), AuthModule],
controllers: [BeaconController],
providers: [BeaconService],
})
Expand Down
2 changes: 2 additions & 0 deletions backend/src/beacon/tests/beacon.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
mockValCacheValues,
} from '../../../../src/mocks/beacon';
import { StatusColor } from '../../../../src/types';
import {AuthModule} from "../../auth.module";

describe('BeaconController', () => {
let controller: BeaconController;
Expand All @@ -33,6 +34,7 @@ describe('BeaconController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
AuthModule,
UtilsModule,
CacheModule.register(),
JwtModule.register({
Expand Down
3 changes: 2 additions & 1 deletion backend/src/logs/logs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { UtilsModule } from '../utils/utils.module';
import { LogsService } from './logs.service';
import { SequelizeModule } from '@nestjs/sequelize';
import { Log } from './entities/log.entity';
import {AuthModule} from "../auth.module";

@Module({
imports: [UtilsModule, SequelizeModule.forFeature([Log])],
imports: [UtilsModule, SequelizeModule.forFeature([Log]), AuthModule],
controllers: [LogsController],
providers: [LogsService],
exports: [LogsService],
Expand Down
2 changes: 2 additions & 0 deletions backend/src/logs/tests/logs.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
mockErrorLog,
mockWarningLog,
} from '../../../../src/mocks/logs';
import {AuthModule} from "../../auth.module";

describe('LogsController', () => {
let logsService: LogsService;
Expand All @@ -34,6 +35,7 @@ describe('LogsController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
AuthModule,
UtilsModule,
SequelizeModule.forFeature([Log]),
SequelizeModule.forRoot({
Expand Down
3 changes: 2 additions & 1 deletion backend/src/node/node.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { NodeService } from './node.service';
import { UtilsModule } from '../utils/utils.module';
import { NodeController } from './node.controller';
import { CacheModule } from '@nestjs/cache-manager';
import {AuthModule} from "../auth.module";

@Module({
imports: [UtilsModule, CacheModule.register()],
imports: [UtilsModule, CacheModule.register(), AuthModule],
controllers: [NodeController],
providers: [NodeService],
})
Expand Down
2 changes: 2 additions & 0 deletions backend/src/node/tests/node.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NodeService } from '../node.service';
import { AxiosResponse } from 'axios/index';
import { of } from 'rxjs';
import { mockDiagnostics } from '../../../../src/mocks/beaconSpec';
import {AuthModule} from "../../auth.module";

describe('NodeController', () => {
let controller: NodeController;
Expand All @@ -28,6 +29,7 @@ describe('NodeController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
AuthModule,
UtilsModule,
CacheModule.register(),
JwtModule.register({
Expand Down
Loading