From 3d23c02eb5a3663e4eed5a8dae0ef0bee59bf1e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:49:18 +0000 Subject: [PATCH] Implement auth API functions (login, register, fetchSession) and enhance linter configuration Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- frontends/nextjs/eslint.config.js | 74 +++++++++++-- .../nextjs/src/app/api/v1/[...slug]/route.ts | 2 +- frontends/nextjs/src/app/page.tsx | 2 +- .../nextjs/src/app/ui/[[...slug]]/page.tsx | 2 +- .../src/hooks/__tests__/useAuth.roles.test.ts | 5 +- .../hooks/__tests__/useAuth.session.test.ts | 24 ++-- frontends/nextjs/src/hooks/auth/auth-store.ts | 26 ++++- .../nextjs/src/hooks/auth/utils/map-user.ts | 2 +- .../nextjs/src/lib/auth/api/fetch-session.ts | 49 ++++++++- frontends/nextjs/src/lib/auth/api/login.ts | 46 +++++++- frontends/nextjs/src/lib/auth/api/register.ts | 103 +++++++++++++++++- .../functions/handle-error.ts | 3 +- 12 files changed, 300 insertions(+), 38 deletions(-) diff --git a/frontends/nextjs/eslint.config.js b/frontends/nextjs/eslint.config.js index d05a5154a..c67ffc832 100644 --- a/frontends/nextjs/eslint.config.js +++ b/frontends/nextjs/eslint.config.js @@ -2,8 +2,25 @@ import js from '@eslint/js' import reactHooks from 'eslint-plugin-react-hooks' import tseslint from 'typescript-eslint' +/** + * MetaBuilder ESLint Configuration + * + * Strict type-checking and code quality rules for the MetaBuilder platform. + * Uses TypeScript ESLint for type-aware linting. + * + * Rule Categories: + * 1. Base rules: TypeScript type-checking and code quality + * 2. Stub file relaxations: Placeholder implementations (warnings only) + * 3. Dynamic renderer relaxations: JSON component system (inherently dynamic) + * 4. Test file relaxations: Test code patterns + * 5. Type definition relaxations: Declaration files + */ export default tseslint.config( { ignores: ['dist', 'node_modules', 'packages/*/dist', 'packages/*/node_modules', '.next/**', 'coverage/**', 'next-env.d.ts'] }, + + // ============================================================================ + // Base Configuration - Strict Rules for Production Code + // ============================================================================ { extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked], files: ['**/*.{ts,tsx}'], @@ -18,12 +35,17 @@ export default tseslint.config( 'react-hooks': reactHooks, }, rules: { + // React Hooks ...reactHooks.configs.recommended.rules, + + // TypeScript Type Safety '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', }], + + // Strict Boolean Expressions - Require explicit comparisons '@typescript-eslint/strict-boolean-expressions': ['error', { allowString: false, allowNumber: false, @@ -33,25 +55,48 @@ export default tseslint.config( allowNullableNumber: false, allowAny: false, }], + + // Promise Handling '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-unnecessary-type-assertion': 'error', - '@typescript-eslint/prefer-nullish-coalescing': 'warn', - '@typescript-eslint/prefer-optional-chain': 'warn', - '@typescript-eslint/no-non-null-assertion': 'error', '@typescript-eslint/require-await': 'error', + + // Type Assertions and Safety + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + + // Unsafe Any Operations '@typescript-eslint/no-unsafe-assignment': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + + // Code Style and Best Practices + '@typescript-eslint/prefer-nullish-coalescing': 'warn', + '@typescript-eslint/prefer-optional-chain': 'warn', + '@typescript-eslint/no-redundant-type-constituents': 'warn', + '@typescript-eslint/consistent-type-imports': ['warn', { + prefer: 'type-imports', + fixStyle: 'separate-type-imports' + }], + + // JavaScript Best Practices 'no-console': ['warn', { allow: ['warn', 'error'] }], 'no-debugger': 'error', 'prefer-const': 'error', 'no-var': 'error', + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'no-throw-literal': 'error', }, }, - // Relaxed rules for stub/integration files that are placeholders + + // ============================================================================ + // Stub/Integration Files - Relaxed Rules (Warnings) + // ============================================================================ + // These files are placeholders for future implementation + // Warnings allow development to continue while tracking technical debt { files: [ 'src/lib/dbal/core/client/dbal-integration/**/*.ts', @@ -73,7 +118,11 @@ export default tseslint.config( '@typescript-eslint/no-non-null-assertion': 'warn', }, }, - // Relaxed rules for dynamic component renderers + + // ============================================================================ + // Dynamic Component Renderers - Relaxed Rules + // ============================================================================ + // JSON component system is inherently dynamic and requires some type flexibility { files: [ 'src/lib/packages/json/render-json-component.tsx', @@ -87,7 +136,11 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-argument': 'warn', }, }, - // Relaxed rules for test files + + // ============================================================================ + // Test Files - Relaxed Rules + // ============================================================================ + // Test files often need more flexibility for mocking and assertions { files: ['**/*.test.ts', '**/*.test.tsx'], rules: { @@ -95,13 +148,18 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-argument': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', '@typescript-eslint/no-unsafe-member-access': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', }, }, - // Relaxed rules for type definition files + + // ============================================================================ + // Type Definition Files - Relaxed Rules + // ============================================================================ { files: ['**/*.d.ts'], rules: { '@typescript-eslint/no-redundant-type-constituents': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', }, }, ) diff --git a/frontends/nextjs/src/app/api/v1/[...slug]/route.ts b/frontends/nextjs/src/app/api/v1/[...slug]/route.ts index a65e1ac54..e4019ba44 100644 --- a/frontends/nextjs/src/app/api/v1/[...slug]/route.ts +++ b/frontends/nextjs/src/app/api/v1/[...slug]/route.ts @@ -18,7 +18,7 @@ * - Entity must be declared in package schema */ -import { NextRequest, NextResponse } from 'next/server' +import type { NextRequest, NextResponse } from 'next/server' import { errorResponse, diff --git a/frontends/nextjs/src/app/page.tsx b/frontends/nextjs/src/app/page.tsx index 07d0bb83c..86ff769d6 100644 --- a/frontends/nextjs/src/app/page.tsx +++ b/frontends/nextjs/src/app/page.tsx @@ -1,4 +1,4 @@ -import { Metadata } from 'next' +import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { getAdapter } from '@/lib/db/core/dbal-client' import { loadJSONPackage } from '@/lib/packages/json/functions/load-json-package' diff --git a/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx b/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx index 2ef91432c..87a489aa2 100644 --- a/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx +++ b/frontends/nextjs/src/app/ui/[[...slug]]/page.tsx @@ -1,4 +1,4 @@ -import { Metadata } from 'next' +import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { UIPageRenderer } from '@/components/ui-page-renderer/UIPageRenderer' diff --git a/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts b/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts index 5ecc99dc5..0a2d50f89 100644 --- a/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts +++ b/frontends/nextjs/src/hooks/__tests__/useAuth.roles.test.ts @@ -87,7 +87,10 @@ describe('useAuth role mapping', () => { ])('applies level for role "$role"', async ({ role, expectedLevel }) => { const { result, unmount } = renderHook(() => useAuth()) - mockLogin.mockResolvedValue(createUser({ role })) + mockLogin.mockResolvedValue({ + success: true, + user: createUser({ role }), + }) await waitForIdle(result) await act(async () => { diff --git a/frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts b/frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts index 18edcc476..eb2c18c24 100644 --- a/frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts +++ b/frontends/nextjs/src/hooks/__tests__/useAuth.session.test.ts @@ -99,7 +99,10 @@ describe('useAuth session flows', () => { it('authenticates on login', async () => { const { result, unmount } = renderHook(() => useAuth()) - mockLogin.mockResolvedValue(createUser()) + mockLogin.mockResolvedValue({ + success: true, + user: createUser(), + }) await waitForIdle(result) await act(async () => { @@ -120,7 +123,10 @@ describe('useAuth session flows', () => { it('clears user on logout', async () => { const { result, unmount } = renderHook(() => useAuth()) - mockLogin.mockResolvedValue(createUser()) + mockLogin.mockResolvedValue({ + success: true, + user: createUser(), + }) await waitForIdle(result) await act(async () => { @@ -140,13 +146,14 @@ describe('useAuth session flows', () => { it('registers and authenticates', async () => { const { result, unmount } = renderHook(() => useAuth()) - mockRegister.mockResolvedValue( - createUser({ + mockRegister.mockResolvedValue({ + success: true, + user: createUser({ id: 'user_2', username: 'newbie', email: 'newbie@example.com', - }) - ) + }), + }) await waitForIdle(result) await act(async () => { @@ -168,7 +175,10 @@ describe('useAuth session flows', () => { const first = renderHook(() => useAuth()) const second = renderHook(() => useAuth()) - mockLogin.mockResolvedValue(createUser({ email: 'sync@example.com', username: 'sync' })) + mockLogin.mockResolvedValue({ + success: true, + user: createUser({ email: 'sync@example.com', username: 'sync' }), + }) await waitForIdle(first.result) await waitForIdle(second.result) diff --git a/frontends/nextjs/src/hooks/auth/auth-store.ts b/frontends/nextjs/src/hooks/auth/auth-store.ts index 6af635873..1dcd0360b 100644 --- a/frontends/nextjs/src/hooks/auth/auth-store.ts +++ b/frontends/nextjs/src/hooks/auth/auth-store.ts @@ -51,9 +51,18 @@ export class AuthStore { }) try { - const user = await loginRequest(identifier, password) + const result = await loginRequest(identifier, password) + + if (!result.success || result.user === null) { + this.setState({ + ...this.state, + isLoading: false, + }) + throw new Error(result.error ?? 'Login failed') + } + this.setState({ - user: mapUserToAuthUser(user), + user: mapUserToAuthUser(result.user), isAuthenticated: true, isLoading: false, }) @@ -73,9 +82,18 @@ export class AuthStore { }) try { - const user = await registerRequest(username, email, password) + const result = await registerRequest(username, email, password) + + if (!result.success || result.user === null) { + this.setState({ + ...this.state, + isLoading: false, + }) + throw new Error(result.error ?? 'Registration failed') + } + this.setState({ - user: mapUserToAuthUser(user), + user: mapUserToAuthUser(result.user), isAuthenticated: true, isLoading: false, }) diff --git a/frontends/nextjs/src/hooks/auth/utils/map-user.ts b/frontends/nextjs/src/hooks/auth/utils/map-user.ts index 5a22b9639..58e16099e 100644 --- a/frontends/nextjs/src/hooks/auth/utils/map-user.ts +++ b/frontends/nextjs/src/hooks/auth/utils/map-user.ts @@ -18,7 +18,7 @@ export const mapUserToAuthUser = (user: User): AuthUser => { username: user.username, role: user.role as AuthUser['role'], level: getRoleLevel(user.role), - tenantId: (user.tenantId !== null && user.tenantId.length > 0) ? user.tenantId : undefined, + tenantId: (user.tenantId !== null && user.tenantId !== undefined && user.tenantId.length > 0) ? user.tenantId : undefined, profilePicture: user.profilePicture ?? undefined, bio: user.bio ?? undefined, isInstanceOwner: user.isInstanceOwner, diff --git a/frontends/nextjs/src/lib/auth/api/fetch-session.ts b/frontends/nextjs/src/lib/auth/api/fetch-session.ts index 2310b566d..0d836790f 100644 --- a/frontends/nextjs/src/lib/auth/api/fetch-session.ts +++ b/frontends/nextjs/src/lib/auth/api/fetch-session.ts @@ -1,10 +1,51 @@ /** - * Fetch current session (stub) + * Fetch current session + * + * Retrieves the current user based on session token from cookies or headers */ import type { User } from '@/lib/types/level-types' +import { getSessionByToken } from '@/lib/db/sessions/getters/get-session-by-token' +import { mapUserRecord } from '@/lib/db/users/map-user-record' +import { getAdapter } from '@/lib/db/core/dbal-client' +import { cookies } from 'next/headers' -export function fetchSession(): Promise { - // TODO: Implement session fetching - return Promise.resolve(null) +/** + * Fetch the current session user + * + * @returns User if session is valid, null otherwise + */ +export async function fetchSession(): Promise { + try { + // Get session token from cookies + const cookieStore = await cookies() + const sessionToken = cookieStore.get('session_token')?.value + + if (sessionToken === undefined || sessionToken === null || sessionToken.length === 0) { + return null + } + + // Get session from token + const session = await getSessionByToken(sessionToken) + + if (session === null || session === undefined) { + return null + } + + // Get user from session + const adapter = getAdapter() + const userRecord = await adapter.findFirst('User', { + where: { id: session.userId }, + }) + + if (userRecord === null || userRecord === undefined) { + return null + } + + const user = mapUserRecord(userRecord as Record) + return user + } catch (error) { + console.error('Error fetching session:', error) + return null + } } diff --git a/frontends/nextjs/src/lib/auth/api/login.ts b/frontends/nextjs/src/lib/auth/api/login.ts index c0389aae1..e8eef3da5 100644 --- a/frontends/nextjs/src/lib/auth/api/login.ts +++ b/frontends/nextjs/src/lib/auth/api/login.ts @@ -1,16 +1,52 @@ /** - * Login API (stub) + * Login API + * + * Authenticates a user and returns user data on success */ import type { User } from '@/lib/types/level-types' +import { authenticateUser } from '@/lib/db/auth/queries/authenticate-user' export interface LoginCredentials { username: string password: string } -export function login(_identifier: string, _password: string): Promise { - // TODO: Implement login - // For now, throw an error to indicate not implemented - return Promise.reject(new Error('Login not implemented')) +export interface LoginResult { + success: boolean + user: User | null + error?: string + requiresPasswordChange?: boolean +} + +export async function login(identifier: string, password: string): Promise { + try { + const result = await authenticateUser(identifier, password) + + if (!result.success) { + return { + success: false, + user: null, + error: result.error === 'invalid_credentials' + ? 'Invalid username or password' + : result.error === 'user_not_found' + ? 'User not found' + : result.error === 'account_locked' + ? 'Account is locked' + : 'Authentication failed', + } + } + + return { + success: true, + user: result.user, + requiresPasswordChange: result.requiresPasswordChange, + } + } catch (error) { + return { + success: false, + user: null, + error: error instanceof Error ? error.message : 'Login failed', + } + } } diff --git a/frontends/nextjs/src/lib/auth/api/register.ts b/frontends/nextjs/src/lib/auth/api/register.ts index dcd806096..a203c6b44 100644 --- a/frontends/nextjs/src/lib/auth/api/register.ts +++ b/frontends/nextjs/src/lib/auth/api/register.ts @@ -1,8 +1,14 @@ /** - * Register API (stub) + * Register API + * + * Creates a new user account with username, email, and password */ import type { User } from '@/lib/types/level-types' +import { getAdapter } from '@/lib/db/core/dbal-client' +import { hashPassword } from '@/lib/db/password/hash-password' +import { getUserByUsername } from '@/lib/db/auth/queries/get-user-by-username' +import { getUserByEmail } from '@/lib/db/auth/queries/get-user-by-email' export interface RegisterData { username: string @@ -10,7 +16,96 @@ export interface RegisterData { password: string } -export function register(_username: string, _email: string, _password: string): Promise { - // TODO: Implement registration - return Promise.reject(new Error('Registration not implemented')) +export interface RegisterResult { + success: boolean + user: User | null + error?: string +} + +export async function register(username: string, email: string, password: string): Promise { + try { + // Validate input + if (username.length === 0 || email.length === 0 || password.length === 0) { + return { + success: false, + user: null, + error: 'Username, email, and password are required', + } + } + + // Check if username already exists + const existingUserByUsername = await getUserByUsername(username) + if (existingUserByUsername !== null) { + return { + success: false, + user: null, + error: 'Username already exists', + } + } + + // Check if email already exists + const existingUserByEmail = await getUserByEmail(email) + if (existingUserByEmail !== null) { + return { + success: false, + user: null, + error: 'Email already exists', + } + } + + // Hash password + const passwordHash = await hashPassword(password) + + // Create user + const adapter = getAdapter() + const userId = crypto.randomUUID() + + await adapter.create('User', { + id: userId, + username, + email, + role: 'user', // Default role + createdAt: BigInt(Date.now()), + isInstanceOwner: false, + }) + + // Create credentials + await adapter.create('Credential', { + username, + passwordHash, + }) + + // Fetch the created user + const userRecord = await adapter.findFirst('User', { + where: { id: userId }, + }) + + if (userRecord === null || userRecord === undefined) { + return { + success: false, + user: null, + error: 'Failed to create user', + } + } + + const user: User = { + id: userId, + username, + email, + role: 'user', + createdAt: Date.now(), + isInstanceOwner: false, + } + + return { + success: true, + user, + } + } catch (error) { + return { + success: false, + user: null, + error: error instanceof Error ? error.message : 'Registration failed', + } + } } diff --git a/frontends/nextjs/src/lib/dbal/core/client/dbal-integration/functions/handle-error.ts b/frontends/nextjs/src/lib/dbal/core/client/dbal-integration/functions/handle-error.ts index 66b1e1701..c124bfb88 100644 --- a/frontends/nextjs/src/lib/dbal/core/client/dbal-integration/functions/handle-error.ts +++ b/frontends/nextjs/src/lib/dbal/core/client/dbal-integration/functions/handle-error.ts @@ -1,5 +1,6 @@ import type { DBALClient as _DBALClient, DBALConfig as _DBALConfig } from '@/dbal' -import { DBALError, DBALErrorCode } from '@/dbal/core/foundation/errors' +import type { DBALErrorCode } from '@/dbal/core/foundation/errors'; +import { DBALError } from '@/dbal/core/foundation/errors' export function handleError(error: unknown): { message: string; code?: DBALErrorCode } { if (error instanceof DBALError) {