diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fd0606c --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# This .env file is used only when using docker compose +# When not using compose, the components load their own .env files + +# postgres +POSTGRES_DB: pek-infinity +POSTGRES_USER: pek-infinity +POSTGRES_PASSWORD: pek-infinity +POSTGRES_HOST: postgres + +# backend +FRONTEND_CALLBACK: http://localhost:3000/login/jwt +JWT_SECRET: secret +AUTHSCH_CLIENT_ID: +AUTHSCH_CLIENT_SECRET: +POSTGRES_PRISMA_URL: postgres://pek-infinity:pek-infinity@postgres/pek-infinity +POSTGRES_URL_NON_POOLING: postgres://pek-infinity:pek-infinity@postgres/pek-infinity + +# frontend +NEXT_PUBLIC_API_URL: http://localhost:3300 +NEXT_PUBLIC_PRIVATE_API_URL: http://backend:3300 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore index b58b603..13566b8 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -3,3 +3,6 @@ /workspace.xml # Editor-based HTTP Client requests /httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml new file mode 100644 index 0000000..541945b --- /dev/null +++ b/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/pek-infinity.iml b/.idea/pek-infinity.iml index 0c8867d..24643cc 100644 --- a/.idea/pek-infinity.iml +++ b/.idea/pek-infinity.iml @@ -2,8 +2,8 @@ - + diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend index 4cc3376..2040b49 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -46,5 +46,5 @@ COPY --from=builder /app/backend/dist/ ./ -EXPOSE 3000 +EXPOSE 3300 CMD ["node", "./main.js"] \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..d31ac6c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,5 @@ +AUTHSCH_CLIENT_ID= +AUTHSCH_CLIENT_SECRET= +FRONTEND_CALLBACK=http://localhost:3000/login/jwt +JWT_SECRET=secret +PORT=3300 \ No newline at end of file diff --git a/backend/openapi.yaml b/backend/openapi.yaml index ff779b4..1e8834a 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -22,6 +22,45 @@ paths: application/json: schema: $ref: "#/components/schemas/Ping" + /api/v4/auth/login: + get: + operationId: AuthController_login + parameters: [] + responses: + "200": + description: "" + "302": + description: Redirects to the AuthSch login page. + /api/v4/auth/callback: + get: + operationId: AuthController_oauthRedirect + parameters: + - name: code + required: true + in: query + schema: {} + responses: + "200": + description: "" + "302": + description: Redirects to the frontend with the JWT in the query string. + /api/v4/auth/me: + get: + operationId: AuthController_me + parameters: [] + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/UserDto" + default: + description: "" + content: + application/json: + schema: + $ref: "#/components/schemas/UserDto" info: title: PÉK API description: Profiles and Groups @@ -41,6 +80,13 @@ components: type: string required: - ping + UserDto: + type: object + properties: + name: + type: string + required: + - name externalDocs: description: Source Code (GitHub) url: https://github.com/kir-dev/pek-infinity diff --git a/backend/package.json b/backend/package.json index b93df91..00de927 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,13 +41,20 @@ } }, "dependencies": { + "@kir-dev/passport-authsch": "^2.0.3", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.4.0", "@prisma/client": "^5.18.0", + "dotenv": "^16.4.5", + "env-var": "^7.5.0", "express": "^4.19.2", "nestjs-prisma": "^0.23.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "prisma": "^5.17.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -60,6 +67,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 4987af2..ff2141c 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { PrismaModule } from 'nestjs-prisma'; +import { AuthModule } from './auth/auth.module'; import { PingModule } from './ping/ping.module'; @Module({ - imports: [PrismaModule.forRoot({ isGlobal: true }), PingModule], + imports: [PrismaModule.forRoot({ isGlobal: true }), PingModule, AuthModule], controllers: [], providers: [], }) diff --git a/backend/src/app.ts b/backend/src/app.ts index a271152..cb66488 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,4 +1,4 @@ -import { writeFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { Logger, VersioningType } from '@nestjs/common'; @@ -21,7 +21,8 @@ export async function bootstrap(): Promise<{ app .setGlobalPrefix('api') - .enableVersioning({ type: VersioningType.URI, defaultVersion: '4' }); + .enableVersioning({ type: VersioningType.URI, defaultVersion: '4' }) + .enableCors(); const config = new DocumentBuilder() .setVersion('v4') @@ -43,6 +44,14 @@ export function writeDocument(document: OpenAPIObject): void { const openApiLogger = new Logger('OpenApiGenerator'); const PATH = join(__dirname, '..', 'openapi.yaml'); + const newDocument = yaml.stringify(document); + const currentDocument = readFileSync(PATH, { encoding: 'utf-8', flag: 'r' }); + + if (newDocument === currentDocument) { + openApiLogger.log('No changes in openapi.yaml'); + return; + } + openApiLogger.log('Writing openapi.yaml'); writeFileSync(PATH, yaml.stringify(document), { encoding: 'utf-8', diff --git a/backend/src/auth/auth.controller.spec.ts b/backend/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..24a9e63 --- /dev/null +++ b/backend/src/auth/auth.controller.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts new file mode 100644 index 0000000..ccc7677 --- /dev/null +++ b/backend/src/auth/auth.controller.ts @@ -0,0 +1,45 @@ +import { CurrentUser } from '@kir-dev/passport-authsch'; +import { Controller, Get, Redirect, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiQuery, ApiResponse } from '@nestjs/swagger'; + +import { FRONTEND_CALLBACK } from '@/config/environment.config'; + +import { AuthService } from './auth.service'; +import { UserDto } from './entities/user.entity'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @UseGuards(AuthGuard('authsch')) + @Get('login') + @ApiResponse({ + status: 302, + description: 'Redirects to the AuthSch login page.', + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + login() {} + + @Get('callback') + @UseGuards(AuthGuard('authsch')) + @Redirect() + @ApiResponse({ + status: 302, + description: 'Redirects to the frontend with the JWT in the query string.', + }) + @ApiQuery({ name: 'code', required: true }) + oauthRedirect(@CurrentUser() user: UserDto) { + const jwt = this.authService.login(user); + return { + url: `${FRONTEND_CALLBACK}?jwt=${jwt}`, + }; + } + + @Get('me') + @UseGuards(AuthGuard('jwt')) + @ApiResponse({ type: UserDto }) + me(@CurrentUser() user: UserDto): UserDto { + return user; + } +} diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..d0c48b2 --- /dev/null +++ b/backend/src/auth/auth.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; + +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { AuthSchStrategy } from './authsch.strategy'; +import { JwtStrategy } from './jwt.strategy'; + +@Module({ + providers: [AuthService, AuthSchStrategy, JwtStrategy], + controllers: [AuthController], + imports: [PassportModule, JwtModule], +}) +export class AuthModule {} diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..5b50966 --- /dev/null +++ b/backend/src/auth/auth.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts new file mode 100644 index 0000000..9a486cc --- /dev/null +++ b/backend/src/auth/auth.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; + +import { JWT_SECRET } from '@/config/environment.config'; + +import { UserDto } from './entities/user.entity'; + +@Injectable() +export class AuthService { + constructor(private jwtService: JwtService) {} + + login(user: UserDto): string { + return this.jwtService.sign(user, { + secret: JWT_SECRET, + expiresIn: '7 days', + }); + } +} diff --git a/backend/src/auth/authsch.strategy.ts b/backend/src/auth/authsch.strategy.ts new file mode 100644 index 0000000..c2ea49b --- /dev/null +++ b/backend/src/auth/authsch.strategy.ts @@ -0,0 +1,31 @@ +import { + AuthSchProfile, + AuthSchScope, + Strategy, +} from '@kir-dev/passport-authsch'; +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; + +import { + AUTHSCH_CLIENT_ID, + AUTHSCH_CLIENT_SECRET, +} from '@/config/environment.config'; + +import { UserDto } from './entities/user.entity'; + +@Injectable() +export class AuthSchStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + clientId: AUTHSCH_CLIENT_ID, + clientSecret: AUTHSCH_CLIENT_SECRET, + scopes: [AuthSchScope.PROFILE, AuthSchScope.PEK_PROFILE], + }); + } + + validate(userProfile: AuthSchProfile): Promise { + return Promise.resolve({ + name: userProfile.fullName, + }); + } +} diff --git a/backend/src/auth/entities/user.entity.ts b/backend/src/auth/entities/user.entity.ts new file mode 100644 index 0000000..06ccc70 --- /dev/null +++ b/backend/src/auth/entities/user.entity.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserDto { + @ApiProperty() + name: string; +} diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..7129ee4 --- /dev/null +++ b/backend/src/auth/jwt.strategy.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +import { JWT_SECRET } from '@/config/environment.config'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor() { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: JWT_SECRET, + }); + } + + validate(payload: unknown): unknown { + return payload; + } +} diff --git a/backend/src/config/environment.config.ts b/backend/src/config/environment.config.ts new file mode 100644 index 0000000..2dc488d --- /dev/null +++ b/backend/src/config/environment.config.ts @@ -0,0 +1,18 @@ +import * as dotenv from 'dotenv'; +import * as env from 'env-var'; + +dotenv.config(); + +export const FRONTEND_CALLBACK = env + .get('FRONTEND_CALLBACK') + .required() + .asString(); +export const JWT_SECRET = env.get('JWT_SECRET').required().asString(); +export const AUTHSCH_CLIENT_ID = env + .get('AUTHSCH_CLIENT_ID') + .required() + .asString(); +export const AUTHSCH_CLIENT_SECRET = env + .get('AUTHSCH_CLIENT_SECRET') + .required() + .asString(); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c482e53 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +services: + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + ports: + - '3000:3000' + env_file: + - .env + + backend: + build: + context: . + dockerfile: Dockerfile.backend + ports: + - '3300:3300' + env_file: + - .env + links: + - postgres + + postgres: + image: postgres:13-alpine + volumes: + - pek-postgres:/var/lib/postgresql/data + env_file: + - .env + +volumes: + pek-postgres: diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..f91f8c9 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=http://localhost:3300 \ No newline at end of file diff --git a/frontend/app/actions.ts b/frontend/app/actions.ts new file mode 100644 index 0000000..08a53b6 --- /dev/null +++ b/frontend/app/actions.ts @@ -0,0 +1,20 @@ +'use server'; + +export async function getBackend({ preferredNetwork }: { preferredNetwork: 'private' | 'public' }): Promise { + const PRIVATE_API = process.env.NEXT_PUBLIC_PRIVATE_API_URL; + const PUBLIC_API = process.env.NEXT_PUBLIC_API_URL; + + const hasPrivateApi = Boolean(PRIVATE_API); + const componentRequestedPrivateApi = preferredNetwork === 'private'; + + if (hasPrivateApi && componentRequestedPrivateApi) return PRIVATE_API!; + + // If this is a vercel preview, use the compiled backend via page router + if (process.env.VERCEL_ENV === 'preview') { + return `https://${process.env.VERCEL_URL}`; + } + + if (PUBLIC_API) return PUBLIC_API; + + throw new Error(`NEXT_PUBLIC_API_URL is not set`); +} diff --git a/frontend/app/debuginfo/debug-client.tsx b/frontend/app/debuginfo/debug-client.tsx new file mode 100644 index 0000000..835a00b --- /dev/null +++ b/frontend/app/debuginfo/debug-client.tsx @@ -0,0 +1,7 @@ +'use client'; +import { useApi } from '@/hooks/use-api'; + +export function PekClientDebug() { + const api = useApi(); + return <>{JSON.stringify(api)}; +} diff --git a/frontend/app/(routes)/debuginfo/page.tsx b/frontend/app/debuginfo/page.tsx similarity index 55% rename from frontend/app/(routes)/debuginfo/page.tsx rename to frontend/app/debuginfo/page.tsx index 6e5a964..af889cd 100644 --- a/frontend/app/(routes)/debuginfo/page.tsx +++ b/frontend/app/debuginfo/page.tsx @@ -1,13 +1,20 @@ import React from 'react'; -import { getBasePath } from '@/pek-api'; +import { getBackend } from '@/app/actions'; -export default function Page() { +import { PekClientDebug } from './debug-client'; + +export default async function Page() { + 'use server'; + const publicApi = await getBackend({ preferredNetwork: 'public' }); + const privateApi = await getBackend({ preferredNetwork: 'private' }); const info = { NODE_ENV: process.env.NODE_ENV, VERCEL_ENV: process.env.VERCEL_ENV, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, - 'basePath()': getBasePath(), + api: publicApi, + privateApi: publicApi === privateApi ? 'same' : 'different', + 'window.pekApi': , }; return ( @@ -21,7 +28,7 @@ export default function Page() { {Object.entries(info).map(([key, value]) => ( - + ))} diff --git a/frontend/app/(routes)/groups/[id]/page.tsx b/frontend/app/groups/[id]/page.tsx similarity index 93% rename from frontend/app/(routes)/groups/[id]/page.tsx rename to frontend/app/groups/[id]/page.tsx index b200e28..4fff8b9 100644 --- a/frontend/app/(routes)/groups/[id]/page.tsx +++ b/frontend/app/groups/[id]/page.tsx @@ -1,7 +1,5 @@ import { Navbar } from '@/components/navbar'; -export const dynamic = 'force-dynamic'; - export default function Page({ params }: { params: { id: string } }) { return ( <> diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 5cfa6bc..06f8f18 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,6 +3,9 @@ import './globals.css'; import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; +import { getBackend } from './actions'; +import { Providers } from './providers'; + const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { @@ -10,14 +13,20 @@ export const metadata: Metadata = { description: 'Generated by create next app', }; -export default function RootLayout({ +export const dynamic = 'force-dynamic'; + +export default async function RootLayoutServer({ children, }: Readonly<{ children: React.ReactNode; }>) { + 'use server'; + const apiBasePath = await getBackend({ preferredNetwork: 'public' }); return ( - {children} + + {children} + ); } diff --git a/frontend/app/login/jwt/route.ts b/frontend/app/login/jwt/route.ts new file mode 100644 index 0000000..0cc2cc1 --- /dev/null +++ b/frontend/app/login/jwt/route.ts @@ -0,0 +1,18 @@ +import { type NextRequest, NextResponse } from 'next/server'; + +export function GET(req: NextRequest): NextResponse { + const oAuthToken = req.nextUrl.searchParams.get('jwt'); + if (oAuthToken) { + const response = NextResponse.redirect(new URL('/', req.url)); + response.cookies.set({ + name: 'jwt', + value: oAuthToken.toString(), + maxAge: 60 * 60 * 24 * 7, + secure: process.env.NODE_ENV === 'production', + }); + + return response; + } + + return NextResponse.next(); +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 0000000..142521c --- /dev/null +++ b/frontend/app/login/page.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { useEffect } from 'react'; + +import { useApi } from '@/hooks/use-api'; + +export default function Page() { + const pek = useApi(); + const url = pek?.loginPath; + + useEffect(() => { + if (url) window.location.href = url; + }, [url]); + +

Redirecting to {url ?? 'backend/login'}

; +} diff --git a/frontend/app/(routes)/page.tsx b/frontend/app/page.tsx similarity index 84% rename from frontend/app/(routes)/page.tsx rename to frontend/app/page.tsx index ab9c0f0..6e28876 100644 --- a/frontend/app/(routes)/page.tsx +++ b/frontend/app/page.tsx @@ -1,11 +1,13 @@ -import { Navbar } from '@/components/navbar'; -import { PekApi } from '@/pek-api'; +import Link from 'next/link'; -export const dynamic = 'force-dynamic'; +import { ClientSideProfile } from '@/components/client-side-profile'; +import { Navbar } from '@/components/navbar'; +import { ServerSideProfile } from '@/components/server-side-profile'; +import { ServerPekApi } from '@/network/server-api'; export default async function Home() { - const api = new PekApi(); - const ping = await api.ping(); + const pek = await ServerPekApi.getDefault(); + const ping = await pek.ping(); return ( <> @@ -43,6 +45,9 @@ export default async function Home() {

Egy random személy profilja

+ Login + + ); diff --git a/frontend/app/(routes)/profiles/[id]/page.tsx b/frontend/app/profiles/[id]/page.tsx similarity index 93% rename from frontend/app/(routes)/profiles/[id]/page.tsx rename to frontend/app/profiles/[id]/page.tsx index 83b4cdf..289f86a 100644 --- a/frontend/app/(routes)/profiles/[id]/page.tsx +++ b/frontend/app/profiles/[id]/page.tsx @@ -1,7 +1,5 @@ import { Navbar } from '@/components/navbar'; -export const dynamic = 'force-dynamic'; - export default function Page({ params }: { params: { id: string } }) { return ( <> diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx new file mode 100644 index 0000000..1483b7f --- /dev/null +++ b/frontend/app/providers.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { createContext, useEffect } from 'react'; + +import { ClientPekApi } from '@/network/client-api'; + +export const ClientApiContext = createContext(null); + +export function Providers({ children, apiBasePath }: Readonly<{ children: React.ReactNode; apiBasePath: string }>) { + const apiInstance = new ClientPekApi(apiBasePath); + useEffect(() => { + if (typeof window !== 'undefined') (window as any).pekApi = apiInstance; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [apiBasePath]); + + return {children}; +} diff --git a/frontend/components/client-side-profile.tsx b/frontend/components/client-side-profile.tsx new file mode 100644 index 0000000..d4ba762 --- /dev/null +++ b/frontend/components/client-side-profile.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useContext, useEffect, useState } from 'react'; + +import { ClientApiContext } from '@/app/providers'; +import { type UserDto } from '@/pek'; + +export function ClientSideProfile() { + const pek = useContext(ClientApiContext); + const [user, setUser] = useState(null); + + useEffect(() => { + if (!pek) return; + pek + .me() + .then(setUser) + .catch(() => {}); + }, [pek]); + + if (!user) { + return null; + } + + return

{user.name}

; +} diff --git a/frontend/components/server-side-profile.tsx b/frontend/components/server-side-profile.tsx new file mode 100644 index 0000000..c66ea18 --- /dev/null +++ b/frontend/components/server-side-profile.tsx @@ -0,0 +1,13 @@ +import { ServerPekApi } from '@/network/server-api'; +import { type UserDto } from '@/pek'; + +export async function ServerSideProfile() { + const pek = await ServerPekApi.getDefault(); + let user: UserDto; + try { + user = await pek.me(); + } catch (e) { + return null; + } + return

{user.name}

; +} diff --git a/frontend/hooks/use-api.ts b/frontend/hooks/use-api.ts new file mode 100644 index 0000000..5f018d5 --- /dev/null +++ b/frontend/hooks/use-api.ts @@ -0,0 +1,8 @@ +'use client'; +import { useContext } from 'react'; + +import { ClientApiContext } from '@/app/providers'; + +export function useApi() { + return useContext(ClientApiContext); +} diff --git a/frontend/network/abstract-api.ts b/frontend/network/abstract-api.ts new file mode 100644 index 0000000..5774ae1 --- /dev/null +++ b/frontend/network/abstract-api.ts @@ -0,0 +1,28 @@ +import { AxiosInstance } from 'axios'; + +import { Configuration, DefaultApi, UserDto } from '@/pek'; + +export class AbstractPekApi { + constructor( + public readonly basePath: string, + public readonly axios?: AxiosInstance + ) {} + + get loginPath() { + return `${this.basePath}/api/v4/auth/login`; + } + + private get api(): DefaultApi { + return new DefaultApi(new Configuration({ basePath: this.basePath }), this.basePath, this.axios); + } + + async ping(): Promise { + const { data } = await this.api.pingControllerSend(); + return data.ping; + } + + async me(): Promise { + const { data } = await this.api.authControllerMe(); + return data; + } +} diff --git a/frontend/network/client-api.ts b/frontend/network/client-api.ts new file mode 100644 index 0000000..caa640a --- /dev/null +++ b/frontend/network/client-api.ts @@ -0,0 +1,20 @@ +'use client'; +import axios from 'axios'; +import Cookies from 'js-cookie'; + +import { AbstractPekApi } from './abstract-api'; + +export const clientAxios = axios.create(); +clientAxios.interceptors.request.use((config) => { + const jwt = Cookies.get('jwt'); + if (jwt) { + config.headers.Authorization = `Bearer ${jwt}`; + } + return config; +}); + +export class ClientPekApi extends AbstractPekApi { + constructor(basePath: string) { + super(basePath, clientAxios); + } +} diff --git a/frontend/network/server-api.ts b/frontend/network/server-api.ts new file mode 100644 index 0000000..2e14963 --- /dev/null +++ b/frontend/network/server-api.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; +import { cookies } from 'next/headers'; + +import { getBackend } from '@/app/actions'; + +import { AbstractPekApi } from './abstract-api'; + +export const serverAxios = axios.create(); +serverAxios.interceptors.request.use((config) => { + const jwt = cookies().get('jwt'); + if (jwt) { + config.headers.Authorization = `Bearer ${jwt.value}`; + } + return config; +}); + +export class ServerPekApi extends AbstractPekApi { + constructor(basePath: string) { + super(basePath, serverAxios); + } + + static async getDefault(): Promise { + const basePath = await getBackend({ preferredNetwork: 'private' }); + return new ServerPekApi(basePath); + } +} diff --git a/frontend/package.json b/frontend/package.json index 141009e..2295094 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "axios": "^1.7.6", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "js-cookie": "^3.0.5", "lucide-react": "^0.414.0", "next": "14.2.5", "react": "^18", @@ -25,6 +26,7 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/frontend/pages/api/[[...slug]].ts b/frontend/pages/api/[[...slug]].ts index 792ebb8..2d4377d 100644 --- a/frontend/pages/api/[[...slug]].ts +++ b/frontend/pages/api/[[...slug]].ts @@ -1,7 +1,11 @@ -import { bootstrap } from 'backend/dist/app.js'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + if (process.env.VERCEL_ENV !== 'preview') { + res.status(404).end(); + return; + } + const { bootstrap } = await import('backend/dist/app.js'); const { app } = await bootstrap(); const server = (await app.init()).getHttpAdapter().getInstance(); diff --git a/frontend/pek-api.ts b/frontend/pek-api.ts deleted file mode 100644 index cb28391..0000000 --- a/frontend/pek-api.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Configuration, DefaultApi } from '../pek-client'; - -export function getBasePath(): string { - if (process.env.NEXT_PUBLIC_API_URL) { - return process.env.NEXT_PUBLIC_API_URL; - } - if (process.env.VERCEL_ENV === 'preview') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- if VERCEL_ENV is preview, VERCEL_URL is set - return `https://${process.env.VERCEL_URL!}`; - } - return 'http://localhost:3000'; -} - -export class PekApi { - private get api(): DefaultApi { - return new DefaultApi(new Configuration({ basePath: getBasePath() })); - } - - async ping(): Promise { - const { data } = await this.api.pingControllerSend(); - return data.ping; - } -} diff --git a/package.json b/package.json index 25988b6..dc579ad 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,13 @@ "frontend" ], "scripts": { - "dev": "concurrently \"yarn workspace backend run build:watch\" \"yarn workspace frontend run dev\" \"yarn run watch:openapi\"", + "dev": "concurrently --names \"backend,frontend,openapi\" -c \"bgBlue.bold,bgGreen.bold,bgYellow.bold\" \"yarn workspace backend run start:dev\" \"yarn workspace frontend run dev\" \"yarn run watch:openapi\"", "build": "yarn workspace backend run build && yarn workspace frontend run build", "lint": "yarn workspace backend run lint && yarn workspace frontend run lint", "lint:fix": "yarn workspace backend run lint:fix && yarn workspace frontend run lint:fix", "format:fix": "npx prettier -c --write .", "generate": "npx @openapitools/openapi-generator-cli generate -i ./backend/openapi.yaml -g typescript-axios -o ./pek-client", - "watch:openapi": "nodemon --watch backend/openapi.yaml --exec \"yarn generate\"" + "watch:openapi": "nodemon --on-change-only --watch backend/openapi.yaml --exec \"yarn generate\"" }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.13.4", diff --git a/pek-client/api.ts b/pek-client/api.ts index 1c6b885..1d45773 100644 --- a/pek-client/api.ts +++ b/pek-client/api.ts @@ -36,6 +36,19 @@ export interface Ping { */ 'ping': string; } +/** + * + * @export + * @interface UserDto + */ +export interface UserDto { + /** + * + * @type {string} + * @memberof UserDto + */ + 'name': string; +} /** * DefaultApi - axios parameter creator @@ -43,6 +56,102 @@ export interface Ping { */ export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authControllerLogin: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/v4/auth/login`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authControllerMe: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/v4/auth/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {any} code + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authControllerOauthRedirect: async (code: any, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'code' is not null or undefined + assertParamExists('authControllerOauthRedirect', 'code', code) + const localVarPath = `/api/v4/auth/callback`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (code !== undefined) { + for (const [key, value] of Object.entries(code)) { + localVarQueryParameter[key] = value; + } + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * # Health check endpoint
This endpoint is a simple health check API designed to confirm that the server is operational. When accessed, it returns a straightforward response indicating that the service is up and running. * @summary @@ -83,6 +192,40 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authControllerLogin(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authControllerLogin(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['DefaultApi.authControllerLogin']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authControllerMe(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authControllerMe(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['DefaultApi.authControllerMe']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @param {any} code + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async authControllerOauthRedirect(code: any, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.authControllerOauthRedirect(code, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['DefaultApi.authControllerOauthRedirect']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * # Health check endpoint
This endpoint is a simple health check API designed to confirm that the server is operational. When accessed, it returns a straightforward response indicating that the service is up and running. * @summary @@ -105,6 +248,31 @@ export const DefaultApiFp = function(configuration?: Configuration) { export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = DefaultApiFp(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authControllerLogin(options?: any): AxiosPromise { + return localVarFp.authControllerLogin(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authControllerMe(options?: any): AxiosPromise { + return localVarFp.authControllerMe(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {any} code + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + authControllerOauthRedirect(code: any, options?: any): AxiosPromise { + return localVarFp.authControllerOauthRedirect(code, options).then((request) => request(axios, basePath)); + }, /** * # Health check endpoint
This endpoint is a simple health check API designed to confirm that the server is operational. When accessed, it returns a straightforward response indicating that the service is up and running. * @summary @@ -124,6 +292,37 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa * @extends {BaseAPI} */ export class DefaultApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public authControllerLogin(options?: RawAxiosRequestConfig) { + return DefaultApiFp(this.configuration).authControllerLogin(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public authControllerMe(options?: RawAxiosRequestConfig) { + return DefaultApiFp(this.configuration).authControllerMe(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {any} code + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public authControllerOauthRedirect(code: any, options?: RawAxiosRequestConfig) { + return DefaultApiFp(this.configuration).authControllerOauthRedirect(code, options).then((request) => request(this.axios, this.basePath)); + } + /** * # Health check endpoint
This endpoint is a simple health check API designed to confirm that the server is operational. When accessed, it returns a straightforward response indicating that the service is up and running. * @summary diff --git a/yarn.lock b/yarn.lock index 4f7c9d9..f469feb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -925,6 +925,16 @@ __metadata: languageName: node linkType: hard +"@kir-dev/passport-authsch@npm:^2.0.3": + version: 2.0.4 + resolution: "@kir-dev/passport-authsch@npm:2.0.4" + dependencies: + axios: "npm:^1.7.4" + passport-strategy: "npm:^1.0.0" + checksum: 10c0/2faa19e997b509dc59be2e33b6323f9a71f78bf6f887641a3e95f890968940907b4412c1b3d8fde17c5db680b1e4d4da6966505cf5198cfb12ff14f8f08fc0a6 + languageName: node + linkType: hard + "@ljharb/through@npm:^2.3.12": version: 2.3.13 resolution: "@ljharb/through@npm:2.3.13" @@ -1113,6 +1123,18 @@ __metadata: languageName: node linkType: hard +"@nestjs/jwt@npm:^10.2.0": + version: 10.2.0 + resolution: "@nestjs/jwt@npm:10.2.0" + dependencies: + "@types/jsonwebtoken": "npm:9.0.5" + jsonwebtoken: "npm:9.0.2" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/81c5cbcb459122b175ad6b50dad83aab7d5dc3beb6122a56c7f985cc1c7838cd1c5eae9d630e95550b95a03e183502a183029e36ba51879c638bd0bad086c056 + languageName: node + linkType: hard + "@nestjs/mapped-types@npm:2.0.5": version: 2.0.5 resolution: "@nestjs/mapped-types@npm:2.0.5" @@ -1130,6 +1152,16 @@ __metadata: languageName: node linkType: hard +"@nestjs/passport@npm:^10.0.3": + version: 10.0.3 + resolution: "@nestjs/passport@npm:10.0.3" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + passport: ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + checksum: 10c0/9e8a6103407852951625e75d0abd82a0f9786d4f27fc7036731ccbac39cbdb4e597a7313e53a266bb1fe1ec36c5193365abeb3264f5d285ba0aaeb23ee8e3f1b + languageName: node + linkType: hard + "@nestjs/platform-express@npm:^10.0.0": version: 10.4.1 resolution: "@nestjs/platform-express@npm:10.4.1" @@ -1919,7 +1951,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^4.17.21": +"@types/express@npm:*, @types/express@npm:^4.17.21": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -1982,6 +2014,13 @@ __metadata: languageName: node linkType: hard +"@types/js-cookie@npm:^3.0.6": + version: 3.0.6 + resolution: "@types/js-cookie@npm:3.0.6" + checksum: 10c0/173afaf5ea9d86c22395b9d2a00b6adb0006dcfef165d6dcb0395cdc32f5a5dcf9c3c60f97194119963a15849b8f85121e1ae730b03e40bc0c29b84396ba22f9 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -1996,6 +2035,24 @@ __metadata: languageName: node linkType: hard +"@types/jsonwebtoken@npm:*": + version: 9.0.6 + resolution: "@types/jsonwebtoken@npm:9.0.6" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/9c29e3896e5fb6056e54d87514643e59e0cfb966ae25171a107776270195bba955f0373e98c8ed6450c145b18984f5df9cf0fcac360f382cec3c7c4d3510b202 + languageName: node + linkType: hard + +"@types/jsonwebtoken@npm:9.0.5": + version: 9.0.5 + resolution: "@types/jsonwebtoken@npm:9.0.5" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/c582b8420586f3b9550f7e34992cb32be300bc953636f3b087ed9c180ce7ea5c2e4b35090be2d57f0d3168cc3ca1074932907caa2afe09f4e9c84cf5c0daefa8 + languageName: node + linkType: hard + "@types/methods@npm:^1.1.4": version: 1.1.4 resolution: "@types/methods@npm:1.1.4" @@ -2035,6 +2092,35 @@ __metadata: languageName: node linkType: hard +"@types/passport-jwt@npm:^4.0.1": + version: 4.0.1 + resolution: "@types/passport-jwt@npm:4.0.1" + dependencies: + "@types/jsonwebtoken": "npm:*" + "@types/passport-strategy": "npm:*" + checksum: 10c0/0ced0eaa7bb379d674821108d9bc6758223f1a5f2b9790ec78d3eaaccce6a58a424cf8ed22b53d813740ec53d929e21d92cf794ef0fb30c732866750763c0d7a + languageName: node + linkType: hard + +"@types/passport-strategy@npm:*": + version: 0.2.38 + resolution: "@types/passport-strategy@npm:0.2.38" + dependencies: + "@types/express": "npm:*" + "@types/passport": "npm:*" + checksum: 10c0/d7d2b1782a0845bd8914250aa9213a23c8d9c2225db46d854b77f2bf0129a789f46d4a5e9ad336eca277fc7e0a051c0a2942da5c864e7c6710763f102d9d4295 + languageName: node + linkType: hard + +"@types/passport@npm:*": + version: 1.0.16 + resolution: "@types/passport@npm:1.0.16" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/7120c1186c8c67e3818683b5b6a4439d102f67da93cc1c7d8f32484f7bf10e8438dd5de0bf571910b23d06caa43dd1ad501933b48618bfaf54e63219500993fe + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.12 resolution: "@types/prop-types@npm:15.7.12" @@ -3074,7 +3160,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.6": +"axios@npm:^1.7.4, axios@npm:^1.7.6": version: 1.7.6 resolution: "axios@npm:1.7.6" dependencies: @@ -3177,9 +3263,12 @@ __metadata: version: 0.0.0-use.local resolution: "backend@workspace:backend" dependencies: + "@kir-dev/passport-authsch": "npm:^2.0.3" "@nestjs/cli": "npm:^10.0.0" "@nestjs/common": "npm:^10.0.0" "@nestjs/core": "npm:^10.0.0" + "@nestjs/jwt": "npm:^10.2.0" + "@nestjs/passport": "npm:^10.0.3" "@nestjs/platform-express": "npm:^10.0.0" "@nestjs/schematics": "npm:^10.0.0" "@nestjs/swagger": "npm:^7.4.0" @@ -3188,16 +3277,21 @@ __metadata: "@types/express": "npm:^4.17.21" "@types/jest": "npm:^29.5.2" "@types/node": "npm:^20.3.1" + "@types/passport-jwt": "npm:^4.0.1" "@types/supertest": "npm:^6.0.0" "@typescript-eslint/eslint-plugin": "npm:^7.0.0" "@typescript-eslint/parser": "npm:^7.0.0" "@vercel/style-guide": "npm:^6.0.0" + dotenv: "npm:^16.4.5" + env-var: "npm:^7.5.0" eslint: "npm:^8.42.0" eslint-config-prettier: "npm:^9.0.0" eslint-plugin-prettier: "npm:^5.0.0" express: "npm:^4.19.2" jest: "npm:^29.5.0" nestjs-prisma: "npm:^0.23.0" + passport: "npm:^0.7.0" + passport-jwt: "npm:^4.0.1" prettier: "npm:^3.0.0" prisma: "npm:^5.17.0" reflect-metadata: "npm:^0.2.0" @@ -3326,6 +3420,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -4231,6 +4332,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -4250,6 +4358,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -4329,6 +4446,13 @@ __metadata: languageName: node linkType: hard +"env-var@npm:^7.5.0": + version: 7.5.0 + resolution: "env-var@npm:7.5.0" + checksum: 10c0/7eb93446417b2a1caaec213ffa1a28686a23d18f9364c9c007c113bedabf4632f8890a95b23022f3ac59351807f8ed27fce3ea68f85462406d7d19b2f6c75e88 + languageName: node + linkType: hard + "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" @@ -5377,6 +5501,7 @@ __metadata: dependencies: "@radix-ui/react-navigation-menu": "npm:^1.2.0" "@radix-ui/react-slot": "npm:^1.1.0" + "@types/js-cookie": "npm:^3.0.6" "@types/node": "npm:^20" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" @@ -5388,6 +5513,7 @@ __metadata: eslint: "npm:^8.0.0" eslint-config-next: "npm:14.2.5" eslint-plugin-react: "npm:^7.35.0" + js-cookie: "npm:^3.0.5" lucide-react: "npm:^0.414.0" next: "npm:14.2.5" postcss: "npm:^8.4.39" @@ -6972,6 +7098,13 @@ __metadata: languageName: node linkType: hard +"js-cookie@npm:^3.0.5": + version: 3.0.5 + resolution: "js-cookie@npm:3.0.5" + checksum: 10c0/04a0e560407b4489daac3a63e231d35f4e86f78bff9d792011391b49c59f721b513411cd75714c418049c8dc9750b20fcddad1ca5a2ca616c3aca4874cce5b3a + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -7125,6 +7258,24 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0": + version: 9.0.2 + resolution: "jsonwebtoken@npm:9.0.2" + dependencies: + jws: "npm:^3.2.2" + lodash.includes: "npm:^4.3.0" + lodash.isboolean: "npm:^3.0.3" + lodash.isinteger: "npm:^4.0.4" + lodash.isnumber: "npm:^3.0.3" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.once: "npm:^4.0.0" + ms: "npm:^2.1.1" + semver: "npm:^7.5.4" + checksum: 10c0/d287a29814895e866db2e5a0209ce730cbc158441a0e5a70d5e940eb0d28ab7498c6bf45029cc8b479639bca94056e9a7f254e2cdb92a2f5750c7f358657a131 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.5": version: 3.3.5 resolution: "jsx-ast-utils@npm:3.3.5" @@ -7137,6 +7288,27 @@ __metadata: languageName: node linkType: hard +"jwa@npm:^1.4.1": + version: 1.4.1 + resolution: "jwa@npm:1.4.1" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/5c533540bf38702e73cf14765805a94027c66a0aa8b16bc3e89d8d905e61a4ce2791e87e21be97d1293a5ee9d4f3e5e47737e671768265ca4f25706db551d5e9 + languageName: node + linkType: hard + +"jws@npm:^3.2.2": + version: 3.2.2 + resolution: "jws@npm:3.2.2" + dependencies: + jwa: "npm:^1.4.1" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/e770704533d92df358adad7d1261fdecad4d7b66fa153ba80d047e03ca0f1f73007ce5ed3fbc04d2eba09ba6e7e6e645f351e08e5ab51614df1b0aa4f384dfff + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -7232,6 +7404,48 @@ __metadata: languageName: node linkType: hard +"lodash.includes@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.includes@npm:4.3.0" + checksum: 10c0/7ca498b9b75bf602d04e48c0adb842dfc7d90f77bcb2a91a2b2be34a723ad24bc1c8b3683ec6b2552a90f216c723cdea530ddb11a3320e08fa38265703978f4b + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7 + languageName: node + linkType: hard + +"lodash.isinteger@npm:^4.0.4": + version: 4.0.4 + resolution: "lodash.isinteger@npm:4.0.4" + checksum: 10c0/4c3e023a2373bf65bf366d3b8605b97ec830bca702a926939bcaa53f8e02789b6a176e7f166b082f9365bfec4121bfeb52e86e9040cb8d450e64c858583f61b7 + languageName: node + linkType: hard + +"lodash.isnumber@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isnumber@npm:3.0.3" + checksum: 10c0/2d01530513a1ee4f72dd79528444db4e6360588adcb0e2ff663db2b3f642d4bb3d687051ae1115751ca9082db4fdef675160071226ca6bbf5f0c123dbf0aa12d + languageName: node + linkType: hard + +"lodash.isplainobject@npm:^4.0.6": + version: 4.0.6 + resolution: "lodash.isplainobject@npm:4.0.6" + checksum: 10c0/afd70b5c450d1e09f32a737bed06ff85b873ecd3d3d3400458725283e3f2e0bb6bf48e67dbe7a309eb371a822b16a26cca4a63c8c52db3fc7dc9d5f9dd324cbb + languageName: node + linkType: hard + +"lodash.isstring@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.isstring@npm:4.0.1" + checksum: 10c0/09eaf980a283f9eef58ef95b30ec7fee61df4d6bf4aba3b5f096869cc58f24c9da17900febc8ffd67819b4e29de29793190e88dc96983db92d84c95fa85d1c92 + languageName: node + linkType: hard + "lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -7246,6 +7460,13 @@ __metadata: languageName: node linkType: hard +"lodash.once@npm:^4.0.0": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04 + languageName: node + linkType: hard + "lodash@npm:4.17.21, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -8152,6 +8373,34 @@ __metadata: languageName: node linkType: hard +"passport-jwt@npm:^4.0.1": + version: 4.0.1 + resolution: "passport-jwt@npm:4.0.1" + dependencies: + jsonwebtoken: "npm:^9.0.0" + passport-strategy: "npm:^1.0.0" + checksum: 10c0/d7e2b472d399f596a1db31310f8e63d10777ab7468b9a378c964156e5f0a772598b007417356ead578cfdaf60dc2bba39a55f0033ca865186fdb2a2b198e2e7e + languageName: node + linkType: hard + +"passport-strategy@npm:1.x.x, passport-strategy@npm:^1.0.0": + version: 1.0.0 + resolution: "passport-strategy@npm:1.0.0" + checksum: 10c0/cf4cd32e1bf2538a239651581292fbb91ccc83973cde47089f00d2014c24bed63d3e65af21da8ddef649a8896e089eb9c3ac9ca639f36c797654ae9ee4ed65e1 + languageName: node + linkType: hard + +"passport@npm:^0.7.0": + version: 0.7.0 + resolution: "passport@npm:0.7.0" + dependencies: + passport-strategy: "npm:1.x.x" + pause: "npm:0.0.1" + utils-merge: "npm:^1.0.1" + checksum: 10c0/08c940b86e4adbfe43e753f8097300a5a9d1ce9a3aa002d7b12d27770943a1a87202c54597c0f04dbfd4117d67de76303433577512fc19c7e364fec37b0d3fc5 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -8211,6 +8460,13 @@ __metadata: languageName: node linkType: hard +"pause@npm:0.0.1": + version: 0.0.1 + resolution: "pause@npm:0.0.1" + checksum: 10c0/f362655dfa7f44b946302c5a033148852ed5d05f744bd848b1c7eae6a543f743e79c7751ee896ba519fd802affdf239a358bb2ea5ca1b1c1e4e916279f83ab75 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.0.1": version: 1.0.1 resolution: "picocolors@npm:1.0.1" @@ -8964,7 +9220,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -10362,7 +10618,7 @@ __metadata: languageName: node linkType: hard -"utils-merge@npm:1.0.1": +"utils-merge@npm:1.0.1, utils-merge@npm:^1.0.1": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" checksum: 10c0/02ba649de1b7ca8854bfe20a82f1dfbdda3fb57a22ab4a8972a63a34553cf7aa51bc9081cf7e001b035b88186d23689d69e71b510e610a09a4c66f68aa95b672
{key}{value?.toString()}{value}