From 1d868d10b3413c3c8567b1774e61870d279603b7 Mon Sep 17 00:00:00 2001
From: admin
Date: Wed, 1 Apr 2026 13:26:19 +0700
Subject: [PATCH] 690401:1326 fix secutities uuid
---
backend/docker-compose.override.yml.example | 4 +-
...800000000-AlignSchemaWithDocumentation.sql | 105 ------------------
backend/src/app.module.ts | 2 +-
backend/src/config/database.config.ts | 4 +-
backend/src/database/seeds/user.seed.ts | 2 +-
.../app/(dashboard)/correspondences/page.tsx | 9 +-
frontend/app/layout.tsx | 9 +-
frontend/components/common/can.tsx | 11 +-
frontend/lib/auth.ts | 8 +-
frontend/lib/stores/auth-store.ts | 4 +-
frontend/next.config.mjs | 12 --
frontend/providers/session-provider.tsx | 10 +-
frontend/providers/theme-provider.tsx | 3 +
frontend/proxy.ts | 42 ++++++-
frontend/types/next-auth.d.ts | 24 ++--
package.json | 2 +-
.../ADR-019-hybrid-identifier-strategy.md | 39 ++-----
17 files changed, 105 insertions(+), 185 deletions(-)
delete mode 100644 backend/migrations/1733800000000-AlignSchemaWithDocumentation.sql
diff --git a/backend/docker-compose.override.yml.example b/backend/docker-compose.override.yml.example
index d624402..90cdaa1 100644
--- a/backend/docker-compose.override.yml.example
+++ b/backend/docker-compose.override.yml.example
@@ -12,5 +12,5 @@ services:
# Override สำหรับ Database (Local Dev)
mariadb:
environment:
- - MYSQL_ROOT_PASSWORD=Center#2025
- - MYSQL_PASSWORD=Center2025
\ No newline at end of file
+ - MYSQL_ROOT_PASSWORD=Center2025
+ - MYSQL_PASSWORD=Center2025
diff --git a/backend/migrations/1733800000000-AlignSchemaWithDocumentation.sql b/backend/migrations/1733800000000-AlignSchemaWithDocumentation.sql
deleted file mode 100644
index d3ff74c..0000000
--- a/backend/migrations/1733800000000-AlignSchemaWithDocumentation.sql
+++ /dev/null
@@ -1,105 +0,0 @@
--- Migration: Align Schema with Documentation
--- Version: 1733800000000
--- Date: 2025-12-10
--- Description: Add missing fields and fix column lengths to match schema v1.5.1
--- ==========================================================
--- Phase 1: Organizations Table Updates
--- ==========================================================
--- Add role_id column to organizations
-ALTER TABLE organizations
-ADD COLUMN role_id INT NULL COMMENT 'Reference to organization_roles table';
-
--- Add foreign key constraint
-ALTER TABLE organizations
-ADD CONSTRAINT fk_organizations_role FOREIGN KEY (role_id) REFERENCES organization_roles(id) ON DELETE
-SET NULL;
-
--- Modify organization_name length from 200 to 255
-ALTER TABLE organizations
-MODIFY COLUMN organization_name VARCHAR(255) NOT NULL COMMENT 'Organization name';
-
--- ==========================================================
--- Phase 2: Users Table Updates (Security Fields)
--- ==========================================================
--- Add failed_attempts for login tracking
-ALTER TABLE users
-ADD COLUMN failed_attempts INT DEFAULT 0 COMMENT 'Number of failed login attempts';
-
--- Add locked_until for account lockout mechanism
-ALTER TABLE users
-ADD COLUMN locked_until DATETIME NULL COMMENT 'Account locked until this timestamp';
-
--- Add last_login_at for audit trail
-ALTER TABLE users
-ADD COLUMN last_login_at TIMESTAMP NULL COMMENT 'Last successful login timestamp';
-
--- ==========================================================
--- Phase 3: Roles Table Updates
--- ==========================================================
--- Modify role_name length from 50 to 100
-ALTER TABLE roles
-MODIFY COLUMN role_name VARCHAR(100) NOT NULL COMMENT 'Role name';
-
--- ==========================================================
--- Verification Queries
--- ==========================================================
--- Verify organizations table structure
-SELECT COLUMN_NAME,
- DATA_TYPE,
- CHARACTER_MAXIMUM_LENGTH,
- IS_NULLABLE,
- COLUMN_COMMENT
-FROM INFORMATION_SCHEMA.COLUMNS
-WHERE TABLE_SCHEMA = DATABASE()
- AND TABLE_NAME = 'organizations'
-ORDER BY ORDINAL_POSITION;
-
--- Verify users table has new security fields
-SELECT COLUMN_NAME,
- DATA_TYPE,
- COLUMN_DEFAULT,
- IS_NULLABLE,
- COLUMN_COMMENT
-FROM INFORMATION_SCHEMA.COLUMNS
-WHERE TABLE_SCHEMA = DATABASE()
- AND TABLE_NAME = 'users'
- AND COLUMN_NAME IN (
- 'failed_attempts',
- 'locked_until',
- 'last_login_at'
- )
-ORDER BY ORDINAL_POSITION;
-
--- Verify roles table role_name length
-SELECT COLUMN_NAME,
- DATA_TYPE,
- CHARACTER_MAXIMUM_LENGTH
-FROM INFORMATION_SCHEMA.COLUMNS
-WHERE TABLE_SCHEMA = DATABASE()
- AND TABLE_NAME = 'roles'
- AND COLUMN_NAME = 'role_name';
-
--- ==========================================================
--- Rollback Script (Use if needed)
--- ==========================================================
-/*
- -- Rollback Phase 3: Roles
- ALTER TABLE roles
- MODIFY COLUMN role_name VARCHAR(50) NOT NULL;
-
- -- Rollback Phase 2: Users
- ALTER TABLE users
- DROP COLUMN last_login_at,
- DROP COLUMN locked_until,
- DROP COLUMN failed_attempts;
-
- -- Rollback Phase 1: Organizations
- ALTER TABLE organizations
- MODIFY COLUMN organization_name VARCHAR(200) NOT NULL;
-
- ALTER TABLE organizations
- DROP FOREIGN KEY fk_organizations_role;
-
- ALTER TABLE organizations
- DROP COLUMN role_id;
- */
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index e5c5ffc..84cb2e7 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -57,7 +57,7 @@ import { MigrationModule } from './modules/migration/migration.module';
// 1. Setup Config Module พร้อม Validation
ConfigModule.forRoot({
isGlobal: true,
- envFilePath: '.env',
+ envFilePath: ['.env', '.env.local'],
load: [redisConfig],
validationSchema: envValidationSchema,
validationOptions: {
diff --git a/backend/src/config/database.config.ts b/backend/src/config/database.config.ts
index bf59afe..7e32903 100644
--- a/backend/src/config/database.config.ts
+++ b/backend/src/config/database.config.ts
@@ -4,8 +4,8 @@ export const databaseConfig: TypeOrmModuleOptions = {
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT || '3306'),
- username: process.env.DB_USERNAME || 'root',
- password: process.env.DB_PASSWORD || 'Center#2025',
+ username: process.env.DB_USERNAME || 'admin',
+ password: process.env.DB_PASSWORD || 'Center2025',
database: process.env.DB_DATABASE || 'lcbp3_dev',
charset: 'utf8mb4',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
diff --git a/backend/src/database/seeds/user.seed.ts b/backend/src/database/seeds/user.seed.ts
index f353af9..9e8b99e 100644
--- a/backend/src/database/seeds/user.seed.ts
+++ b/backend/src/database/seeds/user.seed.ts
@@ -97,7 +97,7 @@ export async function seedUsers(dataSource: DataSource) {
];
const salt = await bcrypt.genSalt();
- const password = await bcrypt.hash('password123', salt); // Default password
+ const password = await bcrypt.hash('Center2025', salt); // Default password (ADR-019 aligned)
for (const u of usersData) {
let user = await userRepo.findOneBy({ username: u.username });
diff --git a/frontend/app/(dashboard)/correspondences/page.tsx b/frontend/app/(dashboard)/correspondences/page.tsx
index c53b031..11309e8 100644
--- a/frontend/app/(dashboard)/correspondences/page.tsx
+++ b/frontend/app/(dashboard)/correspondences/page.tsx
@@ -8,15 +8,16 @@ import { Can } from '@/components/common/can';
export const dynamic = 'force-dynamic';
interface CorrespondencesPageProps {
- searchParams?: {
+ searchParams: Promise<{
type?: string;
- };
+ }>;
}
-export default function CorrespondencesPage({
+export default async function CorrespondencesPage({
searchParams,
}: CorrespondencesPageProps) {
- const isRfaView = searchParams?.type?.toUpperCase() === 'RFA';
+ const params = await searchParams;
+ const isRfaView = params?.type?.toUpperCase() === 'RFA';
const heading = isRfaView ? 'RFAs (Request for Approval)' : 'Correspondences';
const description = isRfaView
? 'Unified list view for RFA documents'
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index b9dcf62..bbdabd6 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -7,6 +7,7 @@ import QueryProvider from '@/providers/query-provider';
import SessionProvider from '@/providers/session-provider'; // ✅ Import เข้ามา
import ThemeProvider from '@/providers/theme-provider';
import { Toaster } from '@/components/ui/sonner';
+import { headers } from 'next/headers';
const inter = Inter({ subsets: ['latin'] });
@@ -19,13 +20,15 @@ interface RootLayoutProps {
children: React.ReactNode;
}
-export default function RootLayout({ children }: RootLayoutProps) {
+export default async function RootLayout({ children }: RootLayoutProps) {
+ const nonce = (await headers()).get('x-nonce') || '';
+
return (
-
-
+
+
{children}
diff --git a/frontend/components/common/can.tsx b/frontend/components/common/can.tsx
index e419cfd..a3078ab 100644
--- a/frontend/components/common/can.tsx
+++ b/frontend/components/common/can.tsx
@@ -2,7 +2,7 @@
'use client';
import { useAuthStore } from '@/lib/stores/auth-store';
-import { ReactNode } from 'react';
+import { ReactNode, useEffect, useState } from 'react';
interface CanProps {
permission?: string;
@@ -18,6 +18,15 @@ interface CanProps {
export function Can({ permission, role, children, fallback = null }: CanProps) {
const { hasPermission, hasRole } = useAuthStore();
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return <>{fallback}>;
+ }
let allowed = true;
diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts
index 99bb173..8415ae9 100644
--- a/frontend/lib/auth.ts
+++ b/frontend/lib/auth.ts
@@ -162,13 +162,13 @@ export const {
}
return {
- id: backendData.user.user_id.toString(),
- publicId: backendData.user.publicId, // ✅ Added (ADR-019 Waived for session)
+ id: backendData.user.publicId, // ✅ Use publicId for session identity (ADR-019)
+ publicId: backendData.user.publicId,
name: `${backendData.user.firstName ?? ''} ${backendData.user.lastName ?? ''}`.trim(),
email: backendData.user.email,
username: backendData.user.username,
- firstName: backendData.user.firstName, // ✅ Added
- lastName: backendData.user.lastName, // ✅ Added
+ firstName: backendData.user.firstName,
+ lastName: backendData.user.lastName,
role: backendData.user.role || 'User',
organizationId: backendData.user.primaryOrganizationId,
accessToken: backendData.access_token,
diff --git a/frontend/lib/stores/auth-store.ts b/frontend/lib/stores/auth-store.ts
index 3546b81..83e985d 100644
--- a/frontend/lib/stores/auth-store.ts
+++ b/frontend/lib/stores/auth-store.ts
@@ -3,8 +3,8 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface User {
- id: string; // Internal stringified INT (for stability)
- publicId?: string; // ADR-019: Public UUIDv7
+ id: string; // publicId (ADR-019)
+ publicId: string;
username: string;
email: string;
firstName: string;
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index 1a80713..36e540f 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -78,18 +78,6 @@ const nextConfig = {
key: 'X-Content-Type-Options',
value: 'nosniff',
},
- {
- key: 'Content-Security-Policy',
- value: [
- "default-src 'self'",
- "script-src 'self' 'unsafe-eval'", // จำเป็นสำหรับ Workflow DSL Engine (new Function())
- "style-src 'self' 'unsafe-inline'",
- "img-src 'self' data: https:",
- "font-src 'self'",
- "connect-src 'self' ws: wss:",
- "frame-src 'self'",
- ].join('; '),
- },
],
},
];
diff --git a/frontend/providers/session-provider.tsx b/frontend/providers/session-provider.tsx
index 8306a01..808b650 100644
--- a/frontend/providers/session-provider.tsx
+++ b/frontend/providers/session-provider.tsx
@@ -4,9 +4,15 @@
import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react';
import { AuthSync } from '@/components/auth/auth-sync';
-export default function SessionProvider({ children }: { children: React.ReactNode }) {
+export default function SessionProvider({
+ children,
+ nonce,
+}: {
+ children: React.ReactNode;
+ nonce?: string;
+}) {
return (
-
+
{children}
diff --git a/frontend/providers/theme-provider.tsx b/frontend/providers/theme-provider.tsx
index f5cd7a7..5420df0 100644
--- a/frontend/providers/theme-provider.tsx
+++ b/frontend/providers/theme-provider.tsx
@@ -4,8 +4,10 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes';
export default function ThemeProvider({
children,
+ nonce,
}: {
children: React.ReactNode;
+ nonce?: string;
}) {
return (
{children}
diff --git a/frontend/proxy.ts b/frontend/proxy.ts
index 96ca4aa..3708b74 100644
--- a/frontend/proxy.ts
+++ b/frontend/proxy.ts
@@ -43,7 +43,47 @@ export default auth((req) => {
}
}
- return NextResponse.next(); // แก้ไขจาก null
+ // 5. Generate CSP with Nonce (Security Rule Tier 1)
+ // ใช้ Nonce Strategy เพื่ออนุญาต Inline Script เฉพาะที่ระบุตัวตนได้ ป้องกัน XSS
+ const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
+
+ let connectSrcApi = 'http://localhost:3001';
+ if (process.env.NEXT_PUBLIC_API_URL) {
+ try {
+ connectSrcApi = new URL(process.env.NEXT_PUBLIC_API_URL).origin;
+ } catch {
+ connectSrcApi = process.env.NEXT_PUBLIC_API_URL;
+ }
+ }
+
+ const cspHeader = `
+ default-src 'self';
+ script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-eval';
+ style-src 'self' 'unsafe-inline';
+ img-src 'self' blob: data: https:;
+ font-src 'self' data:;
+ connect-src 'self' ws: wss: ${connectSrcApi};
+ object-src 'none';
+ base-uri 'self';
+ form-action 'self';
+ frame-ancestors 'none';
+ upgrade-insecure-requests;
+ `
+ .replace(/\s{2,}/g, ' ')
+ .trim();
+
+ const requestHeaders = new Headers(req.headers);
+ requestHeaders.set('x-nonce', nonce);
+ requestHeaders.set('Content-Security-Policy', cspHeader);
+
+ const response = NextResponse.next({
+ request: {
+ headers: requestHeaders,
+ },
+ });
+ response.headers.set('Content-Security-Policy', cspHeader);
+
+ return response;
});
// กำหนดว่า Middleware นี้จะทำงานกับ Route ไหนบ้าง
diff --git a/frontend/types/next-auth.d.ts b/frontend/types/next-auth.d.ts
index 203c369..17a2b36 100644
--- a/frontend/types/next-auth.d.ts
+++ b/frontend/types/next-auth.d.ts
@@ -4,11 +4,11 @@ import _NextAuth, { DefaultSession } from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
- id: string;
- publicId: string; // ✅ Added (ADR-019 Waived for session)
- username: string; // ✅ Added
- firstName: string; // ✅ Added
- lastName: string; // ✅ Added
+ id: string; // publicId (ADR-019)
+ publicId: string;
+ username: string;
+ firstName: string;
+ lastName: string;
role: string;
organizationId?: number;
} & DefaultSession['user'];
@@ -18,11 +18,11 @@ declare module 'next-auth' {
}
interface User {
- id: string;
- publicId: string; // ✅ Added
- username: string; // ✅ Added
- firstName: string; // ✅ Added
- lastName: string; // ✅ Added
+ id: string; // publicId (ADR-019)
+ publicId: string;
+ username: string;
+ firstName: string;
+ lastName: string;
role: string;
organizationId?: number;
accessToken?: string;
@@ -32,8 +32,8 @@ declare module 'next-auth' {
declare module 'next-auth/jwt' {
interface JWT {
- id: string;
- username: string; // ✅ Added
+ id: string; // publicId or username depending on auth flow
+ username: string;
role: string;
organizationId?: number;
accessToken?: string;
diff --git a/package.json b/package.json
index da57ea7..d5e0bc2 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"start:mcp": "node ./scripts/start-mcp.js",
"dev:backend": "pnpm --filter backend start:dev",
"dev:frontend": "pnpm --filter lcbp3-frontend dev",
- "dev": "pnpm run --parallel /dev|start:dev/",
+ "dev": "pnpm run --parallel \"/dev|start:dev/\"",
"prepare": "husky",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
diff --git a/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md b/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md
index 3ff8508..90ae516 100644
--- a/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md
+++ b/specs/06-Decision-Records/ADR-019-hybrid-identifier-strategy.md
@@ -2,7 +2,7 @@
**Status:** Accepted
**Date:** 2026-03-12
-**Version:** 1.8.1
+**Version:** 1.8.2
**Decision Makers:** Development Team, Database Architect
**Related Documents:**
@@ -369,26 +369,6 @@ ALTER TABLE notifications
-- Using regular INDEX instead
```
-### Rollback SQL
-
-```sql
--- Rollback: Remove UUID columns (Non-destructive reverse)
-ALTER TABLE users DROP INDEX idx_users_uuid, DROP COLUMN uuid;
-ALTER TABLE organizations DROP INDEX idx_organizations_uuid, DROP COLUMN uuid;
-ALTER TABLE projects DROP INDEX idx_projects_uuid, DROP COLUMN uuid;
-ALTER TABLE contracts DROP INDEX idx_contracts_uuid, DROP COLUMN uuid;
-ALTER TABLE correspondences DROP INDEX idx_correspondences_uuid, DROP COLUMN uuid;
-ALTER TABLE correspondence_revisions DROP INDEX idx_correspondence_revisions_uuid, DROP COLUMN uuid;
-ALTER TABLE circulations DROP INDEX idx_circulations_uuid, DROP COLUMN uuid;
-ALTER TABLE shop_drawings DROP INDEX idx_shop_drawings_uuid, DROP COLUMN uuid;
-ALTER TABLE shop_drawing_revisions DROP INDEX idx_shop_drawing_revisions_uuid, DROP COLUMN uuid;
-ALTER TABLE contract_drawings DROP INDEX idx_contract_drawings_uuid, DROP COLUMN uuid;
-ALTER TABLE asbuilt_drawings DROP INDEX idx_asbuilt_drawings_uuid, DROP COLUMN uuid;
-ALTER TABLE asbuilt_drawing_revisions DROP INDEX idx_asbuilt_drawing_revisions_uuid, DROP COLUMN uuid;
-ALTER TABLE attachments DROP INDEX idx_attachments_uuid, DROP COLUMN uuid;
-ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
-```
-
---
## Storage Impact Analysis
@@ -530,17 +510,12 @@ type ProjectOption = {
---
-## Waivers & Exceptions
+## 🔄 Change Log
-### 1. AuthStore / Frontend Session User Identity
-
-**Date:** 2026-04-01
-**Scope:** `frontend/lib/stores/auth-store.ts`, `frontend/lib/auth.ts`
-
-**Decision:** ให้คงฟิลด์ `id` (stringified `user_id` INT) ไว้ใน `User` interface ของ `AuthStore` และ `NextAuth Session` เพื่อความเสถียรของระบบ Login ที่ใช้งานได้ดีอยู่แล้ว โดยให้เพิ่ม `publicId` เป็นฟิลด์เสริมแทนการ Replacement (Waive strict ADR-019 compliance for Session Identity only).
-
-**Rationale:** ป้องกันความเสี่ยงในการเปลี่ยน Logic การจัดการ Session ที่อาจส่งผลกระทบต่อระบบ Authentication โดยรวม
-
----
+| Version | Date | Changes | Updated By |
+| ------- | ---------- | ------------------------------------------------------------------- | ----------- |
+| 1.8.2 | 2026-04-01 | Removed Waiver: Session Identity to enforce strict `publicId` usage | Antigravity |
+| 1.8.1 | 2026-03-21 | Added Naming Convention Summary & Transition Strategy | Claude |
+| 1.8.0 | 2026-03-12 | Initial Decision Outcome & Technical Spec | Human Dev |
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน `05-07-hybrid-uuid-implementation-plan.md`_