Implement auth API functions (login, register, fetchSession) and enhance linter configuration

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-06 18:49:18 +00:00
parent a061fb3241
commit 3d23c02eb5
12 changed files with 300 additions and 38 deletions

View File

@@ -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',
},
},
)

View File

@@ -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,

View File

@@ -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'

View File

@@ -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'

View File

@@ -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 () => {

View File

@@ -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)

View File

@@ -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,
})

View File

@@ -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,

View File

@@ -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<User | null> {
// 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<User | null> {
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<string, unknown>)
return user
} catch (error) {
console.error('Error fetching session:', error)
return null
}
}

View File

@@ -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<User> {
// 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<LoginResult> {
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',
}
}
}

View File

@@ -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<User> {
// 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<RegisterResult> {
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',
}
}
}

View File

@@ -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) {