create-carlonicora-app 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/apps/api/package.json +34 -34
- package/template/apps/web/messages/en.json +13 -0
- package/template/apps/web/package.json +30 -29
- package/template/apps/web/scripts/validate-translations.mjs +252 -0
- package/template/apps/web/src/app/[locale]/(main)/(features)/settings/[module]/page.tsx +26 -0
- package/template/apps/web/src/app/[locale]/(main)/(features)/settings/layout.tsx +12 -0
- package/template/apps/web/src/app/[locale]/(main)/(features)/settings/oauth/[clientId]/page.tsx +174 -0
- package/template/apps/web/src/app/[locale]/(main)/(features)/settings/oauth/new/page.tsx +88 -0
- package/template/apps/web/src/app/[locale]/(main)/(features)/settings/oauth/page.tsx +115 -0
- package/template/apps/web/src/app/[locale]/(main)/(features)/settings/page.tsx +24 -0
- package/template/apps/web/src/features/common/components/navigations/CreationDropDown.tsx +1 -79
- package/template/package.json +14 -10
package/package.json
CHANGED
|
@@ -2,39 +2,39 @@
|
|
|
2
2
|
"author": "",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@carlonicora/nestjs-neo4jsonapi": "workspace:*",
|
|
5
|
-
"@aws-sdk/client-s3": "^3.
|
|
6
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
7
|
-
"@azure/msal-node": "^5.0.
|
|
5
|
+
"@aws-sdk/client-s3": "^3.984.0",
|
|
6
|
+
"@aws-sdk/s3-request-presigner": "^3.984.0",
|
|
7
|
+
"@azure/msal-node": "^5.0.3",
|
|
8
8
|
"@azure/openai": "^2.0.0",
|
|
9
9
|
"@azure/storage-blob": "^12.30.0",
|
|
10
10
|
"@fastify/multipart": "^9.4.0",
|
|
11
11
|
"@fastify/static": "^9.0.0",
|
|
12
12
|
"@fastify/swagger": "^9.6.1",
|
|
13
13
|
"@getbrevo/brevo": "^3.0.1",
|
|
14
|
-
"@langchain/aws": "^1.2.
|
|
15
|
-
"@langchain/community": "^1.1.
|
|
16
|
-
"@langchain/core": "^1.1.
|
|
17
|
-
"@langchain/langgraph": "^1.1.
|
|
18
|
-
"@langchain/ollama": "^1.2.
|
|
19
|
-
"@langchain/openai": "^1.2.
|
|
14
|
+
"@langchain/aws": "^1.2.2",
|
|
15
|
+
"@langchain/community": "^1.1.12",
|
|
16
|
+
"@langchain/core": "^1.1.19",
|
|
17
|
+
"@langchain/langgraph": "^1.1.3",
|
|
18
|
+
"@langchain/ollama": "^1.2.2",
|
|
19
|
+
"@langchain/openai": "^1.2.5",
|
|
20
20
|
"@langchain/textsplitters": "^1.0.1",
|
|
21
21
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
22
22
|
"@microsoft/microsoft-graph-types": "^2.43.1",
|
|
23
23
|
"@nestjs/bullmq": "^11.0.4",
|
|
24
24
|
"@nestjs/cli": "^11.0.16",
|
|
25
|
-
"@nestjs/common": "^11.1.
|
|
26
|
-
"@nestjs/config": "^4.0.
|
|
27
|
-
"@nestjs/core": "^11.1.
|
|
25
|
+
"@nestjs/common": "^11.1.13",
|
|
26
|
+
"@nestjs/config": "^4.0.3",
|
|
27
|
+
"@nestjs/core": "^11.1.13",
|
|
28
28
|
"@nestjs/event-emitter": "^3.0.1",
|
|
29
29
|
"@nestjs/jwt": "^11.0.2",
|
|
30
|
-
"@nestjs/microservices": "^11.1.
|
|
30
|
+
"@nestjs/microservices": "^11.1.13",
|
|
31
31
|
"@nestjs/passport": "^11.0.5",
|
|
32
|
-
"@nestjs/platform-fastify": "^11.1.
|
|
33
|
-
"@nestjs/platform-socket.io": "^11.1.
|
|
34
|
-
"@nestjs/schedule": "^6.1.
|
|
35
|
-
"@nestjs/swagger": "^11.2.
|
|
32
|
+
"@nestjs/platform-fastify": "^11.1.13",
|
|
33
|
+
"@nestjs/platform-socket.io": "^11.1.13",
|
|
34
|
+
"@nestjs/schedule": "^6.1.1",
|
|
35
|
+
"@nestjs/swagger": "^11.2.6",
|
|
36
36
|
"@nestjs/throttler": "^6.5.0",
|
|
37
|
-
"@nestjs/websockets": "^11.1.
|
|
37
|
+
"@nestjs/websockets": "^11.1.13",
|
|
38
38
|
"@opentelemetry/api": "^1.9.0",
|
|
39
39
|
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
|
|
40
40
|
"@opentelemetry/instrumentation-fastify": "^0.55.0",
|
|
@@ -47,20 +47,20 @@
|
|
|
47
47
|
"@sendgrid/mail": "^8.1.6",
|
|
48
48
|
"adm-zip": "^0.5.16",
|
|
49
49
|
"async": "^3.2.6",
|
|
50
|
-
"axios": "^1.13.
|
|
50
|
+
"axios": "^1.13.4",
|
|
51
51
|
"bcryptjs": "^3.0.3",
|
|
52
|
-
"bullmq": "^5.67.
|
|
52
|
+
"bullmq": "^5.67.3",
|
|
53
53
|
"cheerio": "^1.2.0",
|
|
54
54
|
"class-transformer": "^0.5.1",
|
|
55
55
|
"class-validator": "^0.14.3",
|
|
56
|
-
"dotenv": "^17.2.
|
|
57
|
-
"fast-xml-parser": "^5.3.
|
|
58
|
-
"fastify": "^5.7.
|
|
56
|
+
"dotenv": "^17.2.4",
|
|
57
|
+
"fast-xml-parser": "^5.3.4",
|
|
58
|
+
"fastify": "^5.7.4",
|
|
59
59
|
"handlebars": "^4.7.8",
|
|
60
60
|
"ioredis": "^5.9.2",
|
|
61
|
-
"jsdom": "^
|
|
61
|
+
"jsdom": "^28.0.0",
|
|
62
62
|
"jszip": "^3.10.1",
|
|
63
|
-
"langchain": "^1.2.
|
|
63
|
+
"langchain": "^1.2.18",
|
|
64
64
|
"lodash": "^4.17.23",
|
|
65
65
|
"marked": "^17.0.1",
|
|
66
66
|
"mathjs": "^15.1.0",
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
"nestjs-cls": "^6.2.0",
|
|
71
71
|
"nestjs-i18n": "^10.6.0",
|
|
72
72
|
"node-tesseract-ocr": "^2.2.1",
|
|
73
|
-
"nodemailer": "^
|
|
73
|
+
"nodemailer": "^8.0.0",
|
|
74
74
|
"officeparser": "^6.0.4",
|
|
75
|
-
"openai": "^6.
|
|
75
|
+
"openai": "^6.18.0",
|
|
76
76
|
"passport": "^0.7.0",
|
|
77
77
|
"passport-jwt": "^4.0.1",
|
|
78
78
|
"pdf-parse": "^2.4.5",
|
|
79
79
|
"pdf2pic": "^3.2.0",
|
|
80
|
-
"pdfjs-dist": "^5.4.
|
|
80
|
+
"pdfjs-dist": "^5.4.624",
|
|
81
81
|
"pino": "^10.3.0",
|
|
82
82
|
"pino-loki": "^3.0.0",
|
|
83
83
|
"pino-pretty": "^13.1.3",
|
|
@@ -100,21 +100,21 @@
|
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@eslint/js": "^9.39.2",
|
|
102
102
|
"@nestjs/schematics": "^11.0.9",
|
|
103
|
-
"@nestjs/testing": "^11.1.
|
|
103
|
+
"@nestjs/testing": "^11.1.13",
|
|
104
104
|
"@types/jsdom": "^27.0.0",
|
|
105
105
|
"@types/microsoft-graph": "^2.40.1",
|
|
106
106
|
"@types/supertest": "^6.0.3",
|
|
107
107
|
"@types/web-push": "^3.6.4",
|
|
108
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
109
|
-
"@typescript-eslint/parser": "^8.
|
|
108
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
109
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
110
110
|
"@vitest/coverage-v8": "^4.0.18",
|
|
111
|
-
"commander": "^14.0.
|
|
111
|
+
"commander": "^14.0.3",
|
|
112
112
|
"esbuild": "^0.27.2",
|
|
113
113
|
"eslint": "^9.39.2",
|
|
114
114
|
"eslint-config-prettier": "^10.1.8",
|
|
115
115
|
"eslint-plugin-prettier": "^5.5.5",
|
|
116
|
-
"glob": "^13.0.
|
|
117
|
-
"globals": "^17.
|
|
116
|
+
"glob": "^13.0.1",
|
|
117
|
+
"globals": "^17.3.0",
|
|
118
118
|
"nodemon": "^3.1.11",
|
|
119
119
|
"source-map-support": "^0.5.21",
|
|
120
120
|
"supertest": "^7.2.2",
|
|
@@ -506,6 +506,19 @@
|
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
508
|
},
|
|
509
|
+
"subscription": {
|
|
510
|
+
"trial_expired_title": "Your Trial Has Expired",
|
|
511
|
+
"trial_expired_description": "Your 14-day free trial has ended. Subscribe to continue using {{name}}.",
|
|
512
|
+
"trial_expiring_title": "Trial Expiring Soon",
|
|
513
|
+
"trial_expiring_tooltip": "Your trial expires in {days} days. Subscribe now to keep your data.",
|
|
514
|
+
"subscribe_now": "Subscribe Now",
|
|
515
|
+
"delete_account": "Delete Account",
|
|
516
|
+
"delete_confirmation_title": "Delete Your Account?",
|
|
517
|
+
"delete_confirmation_description": "This action cannot be undone. All your data, photos, and settings will be permanently deleted.",
|
|
518
|
+
"delete_confirmation_prompt": "Type \"{companyName}\" to confirm",
|
|
519
|
+
"delete_button": "Delete Account Permanently",
|
|
520
|
+
"cancel": "Cancel"
|
|
521
|
+
},
|
|
509
522
|
"content": {
|
|
510
523
|
"news": "Recent Relevant Contents",
|
|
511
524
|
"fields": {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "dotenv -e ../../.env -- next build",
|
|
7
7
|
"dev": "dotenv -e ../../.env -- next dev",
|
|
8
|
-
"lint": "eslint .",
|
|
8
|
+
"lint": "eslint . && pnpm validate-translations",
|
|
9
|
+
"validate-translations": "node scripts/validate-translations.mjs",
|
|
9
10
|
"start": "dotenv -e ../../.env -- next start -H 0.0.0.0",
|
|
10
11
|
"test": "vitest run",
|
|
11
12
|
"test:coverage": "vitest run --coverage",
|
|
@@ -19,30 +20,30 @@
|
|
|
19
20
|
"engines": {
|
|
20
21
|
"node": "22"
|
|
21
22
|
},
|
|
22
|
-
"packageManager": "pnpm@10.28.
|
|
23
|
+
"packageManager": "pnpm@10.28.2",
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@aws-sdk/lib-storage": "^3.
|
|
25
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
26
|
-
"@aws-sdk/xhr-http-handler": "^3.
|
|
25
|
+
"@aws-sdk/lib-storage": "^3.984.0",
|
|
26
|
+
"@aws-sdk/s3-request-presigner": "^3.984.0",
|
|
27
|
+
"@aws-sdk/xhr-http-handler": "^3.984.0",
|
|
27
28
|
"@base-ui/react": "^1.1.0",
|
|
28
29
|
"@carlonicora/nextjs-jsonapi": "workspace:*",
|
|
29
30
|
"@dnd-kit/core": "^6.3.1",
|
|
30
31
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
31
32
|
"@dnd-kit/sortable": "^10.0.0",
|
|
32
33
|
"@dnd-kit/utilities": "^3.2.2",
|
|
33
|
-
"@floating-ui/react": "^0.27.
|
|
34
|
+
"@floating-ui/react": "^0.27.17",
|
|
34
35
|
"@hello-pangea/dnd": "^18.0.1",
|
|
35
36
|
"@hookform/resolvers": "^5.2.2",
|
|
36
37
|
"@mdx-js/loader": "^3.1.1",
|
|
37
38
|
"@mdx-js/react": "^3.1.1",
|
|
38
|
-
"@next/mdx": "^16.1.
|
|
39
|
-
"@next/third-parties": "16.1.
|
|
39
|
+
"@next/mdx": "^16.1.6",
|
|
40
|
+
"@next/third-parties": "16.1.6",
|
|
40
41
|
"@{{name}}/shared": "workspace:*",
|
|
41
|
-
"@stripe/react-stripe-js": "^5.
|
|
42
|
-
"@stripe/stripe-js": "^8.
|
|
42
|
+
"@stripe/react-stripe-js": "^5.6.0",
|
|
43
|
+
"@stripe/stripe-js": "^8.7.0",
|
|
43
44
|
"@tanstack/react-table": "^8.21.3",
|
|
44
45
|
"@types/mdx": "^2.0.13",
|
|
45
|
-
"autoprefixer": "^10.4.
|
|
46
|
+
"autoprefixer": "^10.4.24",
|
|
46
47
|
"class-variance-authority": "^0.7.1",
|
|
47
48
|
"clsx": "^2.1.1",
|
|
48
49
|
"cmdk": "^1.1.1",
|
|
@@ -50,38 +51,38 @@
|
|
|
50
51
|
"cookies-next": "^6.1.1",
|
|
51
52
|
"date-fns": "^4.1.0",
|
|
52
53
|
"embla-carousel-react": "^8.6.0",
|
|
53
|
-
"framer-motion": "^12.
|
|
54
|
+
"framer-motion": "^12.33.0",
|
|
54
55
|
"fs": "^0.0.1-security",
|
|
55
56
|
"fuse.js": "^7.1.0",
|
|
56
57
|
"i18n-iso-countries": "^7.14.0",
|
|
57
|
-
"i18next": "^25.8.
|
|
58
|
+
"i18next": "^25.8.4",
|
|
58
59
|
"input-otp": "^1.4.2",
|
|
59
|
-
"jotai": "^2.
|
|
60
|
+
"jotai": "^2.17.1",
|
|
60
61
|
"jsonwebtoken": "^9.0.3",
|
|
61
62
|
"jszip": "^3.10.1",
|
|
62
63
|
"lodash": "^4.17.23",
|
|
63
64
|
"lucide": "^0.563.0",
|
|
64
65
|
"lucide-react": "^0.563.0",
|
|
65
|
-
"mailparser": "^3.9.
|
|
66
|
+
"mailparser": "^3.9.3",
|
|
66
67
|
"mammoth": "^1.11.0",
|
|
67
|
-
"next": "16.1.
|
|
68
|
-
"next-intl": "^4.
|
|
68
|
+
"next": "16.1.6",
|
|
69
|
+
"next-intl": "^4.8.2",
|
|
69
70
|
"next-share": "^0.27.0",
|
|
70
71
|
"next-themes": "^0.4.6",
|
|
71
72
|
"pako": "^2.1.0",
|
|
72
73
|
"papaparse": "^5.5.3",
|
|
73
|
-
"pdfjs-dist": "^5.4.
|
|
74
|
+
"pdfjs-dist": "^5.4.624",
|
|
74
75
|
"postal-mime": "^2.7.3",
|
|
75
|
-
"react": "19.2.
|
|
76
|
+
"react": "19.2.4",
|
|
76
77
|
"react-cookie": "8.0.1",
|
|
77
78
|
"react-day-picker": "^9.13.0",
|
|
78
|
-
"react-dom": "19.2.
|
|
79
|
-
"react-dropzone": "^14.
|
|
79
|
+
"react-dom": "19.2.4",
|
|
80
|
+
"react-dropzone": "^14.4.0",
|
|
80
81
|
"react-hook-form": "^7.71.1",
|
|
81
82
|
"react-horizontal-scrolling-menu": "^8.2.0",
|
|
82
|
-
"react-i18next": "^16.5.
|
|
83
|
+
"react-i18next": "^16.5.4",
|
|
83
84
|
"react-masonry-css": "^1.0.16",
|
|
84
|
-
"react-resizable-panels": "^4.
|
|
85
|
+
"react-resizable-panels": "^4.6.0",
|
|
85
86
|
"react-spinners": "^0.17.0",
|
|
86
87
|
"react-toastify": "^11.0.5",
|
|
87
88
|
"recharts": "^3.7.0",
|
|
@@ -99,8 +100,8 @@
|
|
|
99
100
|
"yjs": "^13.6.29"
|
|
100
101
|
},
|
|
101
102
|
"devDependencies": {
|
|
102
|
-
"@next/eslint-plugin-next": "16.1.
|
|
103
|
-
"@playwright/test": "^1.58.
|
|
103
|
+
"@next/eslint-plugin-next": "16.1.6",
|
|
104
|
+
"@playwright/test": "^1.58.1",
|
|
104
105
|
"@tailwindcss/postcss": "^4",
|
|
105
106
|
"@tailwindcss/typography": "^0.5.19",
|
|
106
107
|
"@testing-library/dom": "^10.4.1",
|
|
@@ -117,14 +118,14 @@
|
|
|
117
118
|
"@types/negotiator": "^0.6.4",
|
|
118
119
|
"@types/pako": "^2.0.4",
|
|
119
120
|
"@types/papaparse": "^5.5.2",
|
|
120
|
-
"@types/react": "19.2.
|
|
121
|
+
"@types/react": "19.2.13",
|
|
121
122
|
"@types/react-dom": "19.2.3",
|
|
122
123
|
"@types/turndown": "^5.0.6",
|
|
123
|
-
"@vitejs/plugin-react": "^5.1.
|
|
124
|
+
"@vitejs/plugin-react": "^5.1.3",
|
|
124
125
|
"@vitest/coverage-v8": "^4.0.18",
|
|
125
|
-
"dotenv": "^17.2.
|
|
126
|
+
"dotenv": "^17.2.4",
|
|
126
127
|
"eslint": "^9.39.2",
|
|
127
|
-
"eslint-config-next": "16.1.
|
|
128
|
+
"eslint-config-next": "16.1.6",
|
|
128
129
|
"eslint-plugin-i18next": "^6.1.3",
|
|
129
130
|
"eslint-plugin-react": "^7.37.5",
|
|
130
131
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Translation Validation Script
|
|
4
|
+
*
|
|
5
|
+
* Validates that all translation keys used in the codebase exist in the translation files.
|
|
6
|
+
* Reports missing keys as errors with file locations.
|
|
7
|
+
*
|
|
8
|
+
* Usage: pnpm validate-translations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const MESSAGES_PATH = path.join(__dirname, "../messages/en.json");
|
|
19
|
+
const SRC_PATH = path.join(__dirname, "../src");
|
|
20
|
+
const PACKAGES_PATH = path.join(__dirname, "../../../packages");
|
|
21
|
+
|
|
22
|
+
// Patterns to match translation usage
|
|
23
|
+
const USE_TRANSLATIONS_PATTERN = /useTranslations\(\s*["']([^"']*)["']\s*\)/g;
|
|
24
|
+
const USE_TRANSLATIONS_NO_NS_PATTERN = /useTranslations\(\s*\)/g;
|
|
25
|
+
const T_CALL_PATTERN = /\bt\(\s*["']([^"']+)["']/g;
|
|
26
|
+
const GET_TRANSLATIONS_PATTERN = /getTranslations\(\s*["']([^"']*)["']\s*\)/g;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Recursively get all keys from a nested object
|
|
30
|
+
*/
|
|
31
|
+
function getAllKeys(obj, prefix = "") {
|
|
32
|
+
const keys = new Set();
|
|
33
|
+
|
|
34
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
35
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
36
|
+
|
|
37
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
38
|
+
const nestedKeys = getAllKeys(value, fullKey);
|
|
39
|
+
nestedKeys.forEach((k) => keys.add(k));
|
|
40
|
+
} else {
|
|
41
|
+
keys.add(fullKey);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return keys;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a key exists in the translations (including partial namespace matches)
|
|
50
|
+
*/
|
|
51
|
+
function keyExists(key, validKeys) {
|
|
52
|
+
if (validKeys.has(key)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if key is a valid namespace prefix (for dynamic keys)
|
|
57
|
+
for (const validKey of validKeys) {
|
|
58
|
+
if (validKey.startsWith(key + ".")) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract translation keys from a single file
|
|
68
|
+
*/
|
|
69
|
+
function extractKeysFromFile(filePath) {
|
|
70
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
71
|
+
const usages = [];
|
|
72
|
+
|
|
73
|
+
// Find all useTranslations calls with namespaces in this file
|
|
74
|
+
const namespaces = [];
|
|
75
|
+
|
|
76
|
+
// Match useTranslations("namespace")
|
|
77
|
+
let match;
|
|
78
|
+
const contentForNamespace = content;
|
|
79
|
+
|
|
80
|
+
// Reset regex
|
|
81
|
+
USE_TRANSLATIONS_PATTERN.lastIndex = 0;
|
|
82
|
+
while ((match = USE_TRANSLATIONS_PATTERN.exec(contentForNamespace)) !== null) {
|
|
83
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
84
|
+
namespaces.push({ namespace: match[1], line: lineNumber });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Match useTranslations() without namespace
|
|
88
|
+
USE_TRANSLATIONS_NO_NS_PATTERN.lastIndex = 0;
|
|
89
|
+
while ((match = USE_TRANSLATIONS_NO_NS_PATTERN.exec(contentForNamespace)) !== null) {
|
|
90
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
91
|
+
namespaces.push({ namespace: "", line: lineNumber });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Match getTranslations("namespace") for server components
|
|
95
|
+
GET_TRANSLATIONS_PATTERN.lastIndex = 0;
|
|
96
|
+
while ((match = GET_TRANSLATIONS_PATTERN.exec(contentForNamespace)) !== null) {
|
|
97
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
98
|
+
namespaces.push({ namespace: match[1], line: lineNumber });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If no useTranslations found but t() is used, assume no namespace
|
|
102
|
+
if (namespaces.length === 0) {
|
|
103
|
+
namespaces.push({ namespace: "", line: 0 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find all t("key") calls
|
|
107
|
+
T_CALL_PATTERN.lastIndex = 0;
|
|
108
|
+
while ((match = T_CALL_PATTERN.exec(content)) !== null) {
|
|
109
|
+
const key = match[1];
|
|
110
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
111
|
+
|
|
112
|
+
// Skip dynamic keys (containing variables)
|
|
113
|
+
if (key.includes("${") || key.includes("{")) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Find the closest namespace declaration before this t() call
|
|
118
|
+
let activeNamespace = "";
|
|
119
|
+
for (const ns of namespaces) {
|
|
120
|
+
if (ns.line <= lineNumber) {
|
|
121
|
+
activeNamespace = ns.namespace;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build full key
|
|
126
|
+
const fullKey = activeNamespace ? `${activeNamespace}.${key}` : key;
|
|
127
|
+
|
|
128
|
+
usages.push({
|
|
129
|
+
key: fullKey,
|
|
130
|
+
file: filePath,
|
|
131
|
+
line: lineNumber,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return usages;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Recursively find all TypeScript/JavaScript files
|
|
140
|
+
*/
|
|
141
|
+
function findSourceFiles(dir, files = []) {
|
|
142
|
+
if (!fs.existsSync(dir)) {
|
|
143
|
+
return files;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
147
|
+
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const fullPath = path.join(dir, entry.name);
|
|
150
|
+
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
if (
|
|
153
|
+
entry.name === "node_modules" ||
|
|
154
|
+
entry.name === ".next" ||
|
|
155
|
+
entry.name === "coverage" ||
|
|
156
|
+
entry.name === "__tests__" ||
|
|
157
|
+
entry.name === "tests"
|
|
158
|
+
) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
findSourceFiles(fullPath, files);
|
|
162
|
+
} else if (
|
|
163
|
+
entry.isFile() &&
|
|
164
|
+
(entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) &&
|
|
165
|
+
!entry.name.endsWith(".test.ts") &&
|
|
166
|
+
!entry.name.endsWith(".test.tsx") &&
|
|
167
|
+
!entry.name.endsWith(".spec.ts") &&
|
|
168
|
+
!entry.name.endsWith(".spec.tsx") &&
|
|
169
|
+
!entry.name.endsWith(".d.ts")
|
|
170
|
+
) {
|
|
171
|
+
files.push(fullPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return files;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Main validation function
|
|
180
|
+
*/
|
|
181
|
+
function validateTranslations() {
|
|
182
|
+
if (!fs.existsSync(MESSAGES_PATH)) {
|
|
183
|
+
console.error(`Error: Translation file not found at ${MESSAGES_PATH}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const translations = JSON.parse(fs.readFileSync(MESSAGES_PATH, "utf-8"));
|
|
188
|
+
const validKeys = getAllKeys(translations);
|
|
189
|
+
|
|
190
|
+
console.log(`Loaded ${validKeys.size} translation keys from en.json\n`);
|
|
191
|
+
|
|
192
|
+
const sourceFiles = [
|
|
193
|
+
...findSourceFiles(SRC_PATH),
|
|
194
|
+
...findSourceFiles(path.join(PACKAGES_PATH, "nextjs-jsonapi/src")),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
console.log(`Scanning ${sourceFiles.length} source files...\n`);
|
|
198
|
+
|
|
199
|
+
const allUsages = [];
|
|
200
|
+
|
|
201
|
+
for (const file of sourceFiles) {
|
|
202
|
+
const usages = extractKeysFromFile(file);
|
|
203
|
+
allUsages.push(...usages);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const missingKeys = [];
|
|
207
|
+
const checkedKeys = new Set();
|
|
208
|
+
|
|
209
|
+
for (const usage of allUsages) {
|
|
210
|
+
checkedKeys.add(usage.key);
|
|
211
|
+
|
|
212
|
+
if (!keyExists(usage.key, validKeys)) {
|
|
213
|
+
missingKeys.push(usage);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
missingKeys,
|
|
219
|
+
totalKeysChecked: checkedKeys.size,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Run validation
|
|
224
|
+
const result = validateTranslations();
|
|
225
|
+
|
|
226
|
+
console.log(`Checked ${result.totalKeysChecked} unique translation keys\n`);
|
|
227
|
+
|
|
228
|
+
if (result.missingKeys.length > 0) {
|
|
229
|
+
console.error("Missing translation keys found:\n");
|
|
230
|
+
|
|
231
|
+
const byFile = new Map();
|
|
232
|
+
for (const missing of result.missingKeys) {
|
|
233
|
+
const existing = byFile.get(missing.file) || [];
|
|
234
|
+
existing.push(missing);
|
|
235
|
+
byFile.set(missing.file, existing);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const [file, usages] of byFile) {
|
|
239
|
+
const relativePath = path.relative(process.cwd(), file);
|
|
240
|
+
console.error(` ${relativePath}:`);
|
|
241
|
+
for (const usage of usages) {
|
|
242
|
+
console.error(` Line ${usage.line}: "${usage.key}"`);
|
|
243
|
+
}
|
|
244
|
+
console.error("");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.error(`Total: ${result.missingKeys.length} missing key(s)\n`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
} else {
|
|
250
|
+
console.log("All translation keys are valid!\n");
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import SettingsContainer from "@/features/common/components/containers/SettingsContainer";
|
|
2
|
+
import { SettingsProvider } from "@/features/common/contexts/SettingsContext";
|
|
3
|
+
import { generateSpecificMetadata } from "@/utils/metadata";
|
|
4
|
+
import { PageContainer } from "@carlonicora/nextjs-jsonapi/components";
|
|
5
|
+
import { Metadata } from "next";
|
|
6
|
+
import { getTranslations } from "next-intl/server";
|
|
7
|
+
|
|
8
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
9
|
+
const t = await getTranslations();
|
|
10
|
+
|
|
11
|
+
const title = t(`common.settings`);
|
|
12
|
+
|
|
13
|
+
return await generateSpecificMetadata({ title: title });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function SettingsPage(props: { params: Promise<{ module: string }> }) {
|
|
17
|
+
const { module } = await props.params;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SettingsProvider moduleName={module}>
|
|
21
|
+
<PageContainer>
|
|
22
|
+
<SettingsContainer />
|
|
23
|
+
</PageContainer>
|
|
24
|
+
</SettingsProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
|
|
5
|
+
const StripeProvider = dynamic(
|
|
6
|
+
() => import("@carlonicora/nextjs-jsonapi/billing").then((mod) => mod.StripeProvider),
|
|
7
|
+
{ ssr: false }
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
|
|
11
|
+
return <StripeProvider>{children}</StripeProvider>;
|
|
12
|
+
}
|
package/template/apps/web/src/app/[locale]/(main)/(features)/settings/oauth/[clientId]/page.tsx
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
4
|
+
import { useCallback, useState, use } from "react";
|
|
5
|
+
import {
|
|
6
|
+
OAuthClientDetail,
|
|
7
|
+
OAuthClientForm,
|
|
8
|
+
OAuthClientSecretDisplay,
|
|
9
|
+
PageContainer,
|
|
10
|
+
Button,
|
|
11
|
+
Skeleton,
|
|
12
|
+
} from "@carlonicora/nextjs-jsonapi/components";
|
|
13
|
+
import { useOAuthClient } from "@carlonicora/nextjs-jsonapi/client";
|
|
14
|
+
import { OAuthClientCreateRequest } from "@carlonicora/nextjs-jsonapi/core";
|
|
15
|
+
import { ArrowLeft } from "lucide-react";
|
|
16
|
+
import { useTranslations } from "next-intl";
|
|
17
|
+
|
|
18
|
+
interface OAuthClientDetailPageProps {
|
|
19
|
+
params: Promise<{ clientId: string }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function OAuthClientDetailPage({ params }: OAuthClientDetailPageProps) {
|
|
23
|
+
const t = useTranslations();
|
|
24
|
+
const { clientId } = use(params);
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
const searchParams = useSearchParams();
|
|
27
|
+
const isEditMode = searchParams.get("edit") === "true";
|
|
28
|
+
|
|
29
|
+
const { client, isLoading, error, update, deleteClient, regenerateSecret } = useOAuthClient(clientId);
|
|
30
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
31
|
+
const [newClientSecret, setNewClientSecret] = useState<string | null>(null);
|
|
32
|
+
|
|
33
|
+
const handleBack = useCallback(() => {
|
|
34
|
+
router.push("/settings/oauth");
|
|
35
|
+
}, [router]);
|
|
36
|
+
|
|
37
|
+
const handleEdit = useCallback(() => {
|
|
38
|
+
router.push(`/settings/oauth/${clientId}?edit=true`);
|
|
39
|
+
}, [router, clientId]);
|
|
40
|
+
|
|
41
|
+
const handleCancelEdit = useCallback(() => {
|
|
42
|
+
router.push(`/settings/oauth/${clientId}`);
|
|
43
|
+
}, [router, clientId]);
|
|
44
|
+
|
|
45
|
+
const handleSubmit = useCallback(
|
|
46
|
+
async (data: OAuthClientCreateRequest) => {
|
|
47
|
+
setIsSubmitting(true);
|
|
48
|
+
try {
|
|
49
|
+
await update(data);
|
|
50
|
+
router.push(`/settings/oauth/${clientId}`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("Failed to update client:", err);
|
|
53
|
+
} finally {
|
|
54
|
+
setIsSubmitting(false);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[update, router, clientId]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleDelete = useCallback(async () => {
|
|
61
|
+
try {
|
|
62
|
+
await deleteClient();
|
|
63
|
+
router.push("/settings/oauth");
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("Failed to delete client:", err);
|
|
66
|
+
}
|
|
67
|
+
}, [deleteClient, router]);
|
|
68
|
+
|
|
69
|
+
const handleRegenerateSecret = useCallback(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const secret = await regenerateSecret();
|
|
72
|
+
setNewClientSecret(secret);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error("Failed to regenerate secret:", err);
|
|
75
|
+
}
|
|
76
|
+
}, [regenerateSecret]);
|
|
77
|
+
|
|
78
|
+
const handleSecretDismiss = useCallback(() => {
|
|
79
|
+
setNewClientSecret(null);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// Loading state
|
|
83
|
+
if (isLoading && !client) {
|
|
84
|
+
return (
|
|
85
|
+
<PageContainer>
|
|
86
|
+
<div className="space-y-6">
|
|
87
|
+
<div className="flex items-center gap-4">
|
|
88
|
+
<Button variant="ghost" size="icon" onClick={handleBack}>
|
|
89
|
+
<ArrowLeft className="h-4 w-4" />
|
|
90
|
+
</Button>
|
|
91
|
+
<Skeleton className="h-8 w-48" />
|
|
92
|
+
</div>
|
|
93
|
+
<Skeleton className="h-96 w-full" />
|
|
94
|
+
</div>
|
|
95
|
+
</PageContainer>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Error state
|
|
100
|
+
if (error || !client) {
|
|
101
|
+
return (
|
|
102
|
+
<PageContainer>
|
|
103
|
+
<div className="space-y-6">
|
|
104
|
+
<div className="flex items-center gap-4">
|
|
105
|
+
<Button variant="ghost" size="icon" onClick={handleBack}>
|
|
106
|
+
<ArrowLeft className="h-4 w-4" />
|
|
107
|
+
</Button>
|
|
108
|
+
<h1 className="text-2xl font-bold">{t("oauth.settings.title")}</h1>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center">
|
|
111
|
+
<p className="text-destructive">
|
|
112
|
+
{error?.message || t("oauth.settings.failed_to_load")}
|
|
113
|
+
</p>
|
|
114
|
+
<Button className="mt-4" onClick={handleBack}>
|
|
115
|
+
{t("oauth.settings.back_to_applications")}
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</PageContainer>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Edit mode
|
|
124
|
+
if (isEditMode) {
|
|
125
|
+
return (
|
|
126
|
+
<PageContainer>
|
|
127
|
+
<div className="space-y-6">
|
|
128
|
+
<div className="flex items-center gap-4">
|
|
129
|
+
<Button variant="ghost" size="icon" onClick={handleCancelEdit}>
|
|
130
|
+
<ArrowLeft className="h-4 w-4" />
|
|
131
|
+
</Button>
|
|
132
|
+
<h1 className="text-2xl font-bold">{t("oauth.settings.edit_title", { name: client.name })}</h1>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<OAuthClientForm
|
|
136
|
+
client={client}
|
|
137
|
+
onSubmit={handleSubmit}
|
|
138
|
+
onCancel={handleCancelEdit}
|
|
139
|
+
isLoading={isSubmitting}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</PageContainer>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Detail view
|
|
147
|
+
return (
|
|
148
|
+
<PageContainer>
|
|
149
|
+
<div className="space-y-6">
|
|
150
|
+
<div className="flex items-center gap-4">
|
|
151
|
+
<Button variant="ghost" size="icon" onClick={handleBack}>
|
|
152
|
+
<ArrowLeft className="h-4 w-4" />
|
|
153
|
+
</Button>
|
|
154
|
+
<h1 className="text-2xl font-bold">{client.name}</h1>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<OAuthClientDetail
|
|
158
|
+
client={client}
|
|
159
|
+
isLoading={isLoading}
|
|
160
|
+
onEdit={handleEdit}
|
|
161
|
+
onDelete={handleDelete}
|
|
162
|
+
onRegenerateSecret={handleRegenerateSecret}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Show Secret Dialog */}
|
|
167
|
+
<OAuthClientSecretDisplay
|
|
168
|
+
secret={newClientSecret || ""}
|
|
169
|
+
open={!!newClientSecret}
|
|
170
|
+
onDismiss={handleSecretDismiss}
|
|
171
|
+
/>
|
|
172
|
+
</PageContainer>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useCallback, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
OAuthClientForm,
|
|
7
|
+
OAuthClientSecretDisplay,
|
|
8
|
+
PageContainer,
|
|
9
|
+
Button,
|
|
10
|
+
} from "@carlonicora/nextjs-jsonapi/components";
|
|
11
|
+
import { useOAuthClients } from "@carlonicora/nextjs-jsonapi/client";
|
|
12
|
+
import { OAuthClientCreateRequest } from "@carlonicora/nextjs-jsonapi/core";
|
|
13
|
+
import { ArrowLeft } from "lucide-react";
|
|
14
|
+
import { useTranslations } from "next-intl";
|
|
15
|
+
|
|
16
|
+
export default function OAuthNewClientPage() {
|
|
17
|
+
const t = useTranslations();
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const { createClient } = useOAuthClients();
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
+
const [newClientSecret, setNewClientSecret] = useState<{ clientId: string; secret: string } | null>(null);
|
|
22
|
+
const [createdClientId, setCreatedClientId] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const handleSubmit = useCallback(
|
|
25
|
+
async (data: OAuthClientCreateRequest) => {
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const result = await createClient(data);
|
|
29
|
+
|
|
30
|
+
// Store secret for display
|
|
31
|
+
if (result.clientSecret) {
|
|
32
|
+
setNewClientSecret({
|
|
33
|
+
clientId: result.client.clientId,
|
|
34
|
+
secret: result.clientSecret,
|
|
35
|
+
});
|
|
36
|
+
setCreatedClientId(result.client.id || result.client.clientId);
|
|
37
|
+
} else {
|
|
38
|
+
// No secret (shouldn't happen for new clients)
|
|
39
|
+
router.push(`/settings/oauth/${result.client.id || result.client.clientId}`);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error("Failed to create client:", err);
|
|
43
|
+
} finally {
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
[createClient, router]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleCancel = useCallback(() => {
|
|
51
|
+
router.push("/settings/oauth");
|
|
52
|
+
}, [router]);
|
|
53
|
+
|
|
54
|
+
const handleSecretDismiss = useCallback(() => {
|
|
55
|
+
setNewClientSecret(null);
|
|
56
|
+
if (createdClientId) {
|
|
57
|
+
router.push(`/settings/oauth/${createdClientId}`);
|
|
58
|
+
} else {
|
|
59
|
+
router.push("/settings/oauth");
|
|
60
|
+
}
|
|
61
|
+
}, [router, createdClientId]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<PageContainer>
|
|
65
|
+
<div className="space-y-6">
|
|
66
|
+
<div className="flex items-center gap-4">
|
|
67
|
+
<Button variant="ghost" size="icon" onClick={handleCancel}>
|
|
68
|
+
<ArrowLeft className="h-4 w-4" />
|
|
69
|
+
</Button>
|
|
70
|
+
<h1 className="text-2xl font-bold">{t("oauth.settings.create_title")}</h1>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<OAuthClientForm
|
|
74
|
+
onSubmit={handleSubmit}
|
|
75
|
+
onCancel={handleCancel}
|
|
76
|
+
isLoading={isLoading}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Show Secret Dialog */}
|
|
81
|
+
<OAuthClientSecretDisplay
|
|
82
|
+
secret={newClientSecret?.secret || ""}
|
|
83
|
+
open={!!newClientSecret}
|
|
84
|
+
onDismiss={handleSecretDismiss}
|
|
85
|
+
/>
|
|
86
|
+
</PageContainer>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useCallback, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
OAuthClientList,
|
|
7
|
+
OAuthClientSecretDisplay,
|
|
8
|
+
PageContainer,
|
|
9
|
+
AlertDialog,
|
|
10
|
+
AlertDialogAction,
|
|
11
|
+
AlertDialogCancel,
|
|
12
|
+
AlertDialogContent,
|
|
13
|
+
AlertDialogDescription,
|
|
14
|
+
AlertDialogFooter,
|
|
15
|
+
AlertDialogHeader,
|
|
16
|
+
AlertDialogTitle,
|
|
17
|
+
} from "@carlonicora/nextjs-jsonapi/components";
|
|
18
|
+
import { useOAuthClients } from "@carlonicora/nextjs-jsonapi/client";
|
|
19
|
+
import { OAuthClientInterface, OAuthService } from "@carlonicora/nextjs-jsonapi/core";
|
|
20
|
+
import { useTranslations } from "next-intl";
|
|
21
|
+
|
|
22
|
+
export default function OAuthSettingsPage() {
|
|
23
|
+
const t = useTranslations();
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const { clients, isLoading, error, refetch } = useOAuthClients();
|
|
26
|
+
const [newClientSecret, setNewClientSecret] = useState<{ clientId: string; secret: string } | null>(null);
|
|
27
|
+
const [deleteClient, setDeleteClient] = useState<OAuthClientInterface | null>(null);
|
|
28
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handleClientClick = useCallback(
|
|
31
|
+
(client: OAuthClientInterface) => {
|
|
32
|
+
router.push(`/settings/oauth/${client.id || client.clientId}`);
|
|
33
|
+
},
|
|
34
|
+
[router]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const handleCreateClick = useCallback(() => {
|
|
38
|
+
router.push("/settings/oauth/new");
|
|
39
|
+
}, [router]);
|
|
40
|
+
|
|
41
|
+
const handleEditClick = useCallback(
|
|
42
|
+
(client: OAuthClientInterface) => {
|
|
43
|
+
router.push(`/settings/oauth/${client.id || client.clientId}?edit=true`);
|
|
44
|
+
},
|
|
45
|
+
[router]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const handleDeleteClick = useCallback((client: OAuthClientInterface) => {
|
|
49
|
+
setDeleteClient(client);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const handleConfirmDelete = useCallback(async () => {
|
|
53
|
+
if (!deleteClient) return;
|
|
54
|
+
|
|
55
|
+
setIsDeleting(true);
|
|
56
|
+
try {
|
|
57
|
+
await OAuthService.deleteClient({ clientId: deleteClient.clientId });
|
|
58
|
+
await refetch();
|
|
59
|
+
setDeleteClient(null);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("Failed to delete client:", err);
|
|
62
|
+
} finally {
|
|
63
|
+
setIsDeleting(false);
|
|
64
|
+
}
|
|
65
|
+
}, [deleteClient, refetch]);
|
|
66
|
+
|
|
67
|
+
const handleSecretDismiss = useCallback(() => {
|
|
68
|
+
setNewClientSecret(null);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<PageContainer>
|
|
73
|
+
<OAuthClientList
|
|
74
|
+
clients={clients}
|
|
75
|
+
isLoading={isLoading}
|
|
76
|
+
error={error}
|
|
77
|
+
onClientClick={handleClientClick}
|
|
78
|
+
onCreateClick={handleCreateClick}
|
|
79
|
+
onEditClick={handleEditClick}
|
|
80
|
+
onDeleteClick={handleDeleteClick}
|
|
81
|
+
title="OAuth Applications"
|
|
82
|
+
emptyStateMessage="Create OAuth applications to allow third-party integrations."
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* New Client Secret Display */}
|
|
86
|
+
<OAuthClientSecretDisplay
|
|
87
|
+
secret={newClientSecret?.secret || ""}
|
|
88
|
+
open={!!newClientSecret}
|
|
89
|
+
onDismiss={handleSecretDismiss}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
{/* Delete Confirmation Dialog */}
|
|
93
|
+
<AlertDialog open={!!deleteClient} onOpenChange={(open) => !open && setDeleteClient(null)}>
|
|
94
|
+
<AlertDialogContent>
|
|
95
|
+
<AlertDialogHeader>
|
|
96
|
+
<AlertDialogTitle>{t("oauth.settings.delete_title")}</AlertDialogTitle>
|
|
97
|
+
<AlertDialogDescription>
|
|
98
|
+
{t("oauth.settings.delete_description", { name: deleteClient?.name || "" })}
|
|
99
|
+
</AlertDialogDescription>
|
|
100
|
+
</AlertDialogHeader>
|
|
101
|
+
<AlertDialogFooter>
|
|
102
|
+
<AlertDialogCancel disabled={isDeleting}>{t("ui.buttons.cancel")}</AlertDialogCancel>
|
|
103
|
+
<AlertDialogAction
|
|
104
|
+
onClick={handleConfirmDelete}
|
|
105
|
+
disabled={isDeleting}
|
|
106
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
107
|
+
>
|
|
108
|
+
{isDeleting ? t("ui.buttons.deleting") : t("ui.buttons.delete")}
|
|
109
|
+
</AlertDialogAction>
|
|
110
|
+
</AlertDialogFooter>
|
|
111
|
+
</AlertDialogContent>
|
|
112
|
+
</AlertDialog>
|
|
113
|
+
</PageContainer>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import SettingsContainer from "@/features/common/components/containers/SettingsContainer";
|
|
2
|
+
import { SettingsProvider } from "@/features/common/contexts/SettingsContext";
|
|
3
|
+
import { generateSpecificMetadata } from "@/utils/metadata";
|
|
4
|
+
import { PageContainer } from "@carlonicora/nextjs-jsonapi/components";
|
|
5
|
+
import { Metadata } from "next";
|
|
6
|
+
import { getTranslations } from "next-intl/server";
|
|
7
|
+
|
|
8
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
9
|
+
const t = await getTranslations();
|
|
10
|
+
|
|
11
|
+
const title = t(`common.settings`);
|
|
12
|
+
|
|
13
|
+
return await generateSpecificMetadata({ title: title });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function SettingsPage() {
|
|
17
|
+
return (
|
|
18
|
+
<SettingsProvider>
|
|
19
|
+
<PageContainer>
|
|
20
|
+
<SettingsContainer />
|
|
21
|
+
</PageContainer>
|
|
22
|
+
</SettingsProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
DropdownMenuTrigger,
|
|
10
10
|
useSidebar,
|
|
11
11
|
} from "@carlonicora/nextjs-jsonapi/components";
|
|
12
|
-
import { useCurrentUserContext } from "@carlonicora/nextjs-jsonapi/contexts";
|
|
13
|
-
import { UserInterface } from "@carlonicora/nextjs-jsonapi/core";
|
|
14
12
|
import { PlusCircleIcon } from "lucide-react";
|
|
15
13
|
import { useTranslations } from "next-intl";
|
|
16
14
|
import { useState } from "react";
|
|
@@ -18,51 +16,9 @@ import { useState } from "react";
|
|
|
18
16
|
export default function CreationDropDown() {
|
|
19
17
|
const { state } = useSidebar();
|
|
20
18
|
const t = useTranslations();
|
|
21
|
-
const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
|
|
22
19
|
|
|
23
20
|
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
|
24
21
|
|
|
25
|
-
// const [newArticleOpen, setNewArticleOpen] = useState<boolean>(false);
|
|
26
|
-
// const [newHyperlinkOpen, setNewHyperlinkOpen] = useState<boolean>(false);
|
|
27
|
-
// const [newDocumentOpen, setNewDocumentOpen] = useState<boolean>(false);
|
|
28
|
-
// const [newGlossaryOpen, setNewGlossaryOpen] = useState<boolean>(false);
|
|
29
|
-
// const [newDiscussionOpen, setNewDiscussionOpen] = useState<boolean>(false);
|
|
30
|
-
|
|
31
|
-
// const handleArticleClick = () => {
|
|
32
|
-
// setMenuOpen(false);
|
|
33
|
-
// requestAnimationFrame(() => {
|
|
34
|
-
// setNewArticleOpen(true);
|
|
35
|
-
// });
|
|
36
|
-
// };
|
|
37
|
-
|
|
38
|
-
// const handleHyperlinkClick = () => {
|
|
39
|
-
// setMenuOpen(false);
|
|
40
|
-
// requestAnimationFrame(() => {
|
|
41
|
-
// setNewHyperlinkOpen(true);
|
|
42
|
-
// });
|
|
43
|
-
// };
|
|
44
|
-
|
|
45
|
-
// const handleDocumentClick = () => {
|
|
46
|
-
// setMenuOpen(false);
|
|
47
|
-
// requestAnimationFrame(() => {
|
|
48
|
-
// setNewDocumentOpen(true);
|
|
49
|
-
// });
|
|
50
|
-
// };
|
|
51
|
-
|
|
52
|
-
// const handleGlossaryClick = () => {
|
|
53
|
-
// setMenuOpen(false);
|
|
54
|
-
// requestAnimationFrame(() => {
|
|
55
|
-
// setNewGlossaryOpen(true);
|
|
56
|
-
// });
|
|
57
|
-
// };
|
|
58
|
-
|
|
59
|
-
// const handleDiscussionClick = () => {
|
|
60
|
-
// setMenuOpen(false);
|
|
61
|
-
// requestAnimationFrame(() => {
|
|
62
|
-
// setNewDiscussionOpen(true);
|
|
63
|
-
// });
|
|
64
|
-
// };
|
|
65
|
-
|
|
66
22
|
return (
|
|
67
23
|
<>
|
|
68
24
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
|
@@ -75,43 +31,9 @@ export default function CreationDropDown() {
|
|
|
75
31
|
<DropdownMenuContent align="start" className="w-96">
|
|
76
32
|
<DropdownMenuLabel>{t(`common.create_new`)}</DropdownMenuLabel>
|
|
77
33
|
<DropdownMenuSeparator />
|
|
78
|
-
{/*
|
|
79
|
-
{getIconByModule({ module: Modules.Article })}
|
|
80
|
-
{t(`entities.articles`, { count: 1 })}
|
|
81
|
-
</DropdownMenuItem>
|
|
82
|
-
<DropdownMenuItem onClick={handleHyperlinkClick} className="flex items-center gap-2 font-normal">
|
|
83
|
-
{getIconByModule({ module: Modules.Hyperlink })}
|
|
84
|
-
{t(`entities.hyperlinks`, { count: 1 })}
|
|
85
|
-
</DropdownMenuItem>
|
|
86
|
-
<DropdownMenuItem onClick={handleDocumentClick} className="flex items-center gap-2 font-normal">
|
|
87
|
-
{getIconByModule({ module: Modules.Document })}
|
|
88
|
-
{t(`entities.documents`, { count: 1 })}
|
|
89
|
-
</DropdownMenuItem>
|
|
90
|
-
<DropdownMenuItem onClick={handleGlossaryClick} className="flex items-center gap-2 font-normal">
|
|
91
|
-
{getIconByModule({ module: Modules.Glossary })}
|
|
92
|
-
{t(`entities.glossaries`, { count: 1 })}
|
|
93
|
-
</DropdownMenuItem>
|
|
94
|
-
<DropdownMenuItem onClick={handleDiscussionClick} className="flex items-center gap-2 font-normal">
|
|
95
|
-
{getIconByModule({ module: Modules.Discussion })}
|
|
96
|
-
{t(`entities.discussions`, { count: 1 })}
|
|
97
|
-
</DropdownMenuItem> */}
|
|
34
|
+
{/* Add your entity creation menu items here */}
|
|
98
35
|
</DropdownMenuContent>
|
|
99
36
|
</DropdownMenu>
|
|
100
|
-
{/* {hasPermissionToModule({ module: Modules.Article, action: Action.Create }) && (
|
|
101
|
-
<ArticleEditor dialogOpen={newArticleOpen} onDialogOpenChange={setNewArticleOpen} />
|
|
102
|
-
)}
|
|
103
|
-
{hasPermissionToModule({ module: Modules.Hyperlink, action: Action.Create }) && (
|
|
104
|
-
<HyperlinkEditor dialogOpen={newHyperlinkOpen} onDialogOpenChange={setNewHyperlinkOpen} />
|
|
105
|
-
)}
|
|
106
|
-
{hasPermissionToModule({ module: Modules.Document, action: Action.Create }) && (
|
|
107
|
-
<DocumentEditor dialogOpen={newDocumentOpen} onDialogOpenChange={setNewDocumentOpen} />
|
|
108
|
-
)}
|
|
109
|
-
{hasPermissionToModule({ module: Modules.Glossary, action: Action.Create }) && (
|
|
110
|
-
<GlossaryEditor dialogOpen={newGlossaryOpen} onDialogOpenChange={setNewGlossaryOpen} />
|
|
111
|
-
)}
|
|
112
|
-
{hasPermissionToModule({ module: Modules.Discussion, action: Action.Create }) && (
|
|
113
|
-
<DiscussionEditor dialogOpen={newDiscussionOpen} onDialogOpenChange={setNewDiscussionOpen} />
|
|
114
|
-
)} */}
|
|
115
37
|
</>
|
|
116
38
|
);
|
|
117
39
|
}
|
package/template/package.json
CHANGED
|
@@ -45,18 +45,18 @@
|
|
|
45
45
|
"@semantic-release/git": "^10.0.1",
|
|
46
46
|
"@semantic-release/npm": "^13.1.3",
|
|
47
47
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
48
|
-
"@types/node": "^25.
|
|
49
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
50
|
-
"@typescript-eslint/parser": "^8.
|
|
48
|
+
"@types/node": "^25.2.1",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
50
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
51
51
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
52
52
|
"eslint": "^9.39.2",
|
|
53
53
|
"eslint-config-prettier": "^10.1.8",
|
|
54
54
|
"eslint-plugin-prettier": "^5.5.5",
|
|
55
55
|
"husky": "^9.1.7",
|
|
56
56
|
"prettier": "^3.8.1",
|
|
57
|
-
"semantic-release": "^25.0.
|
|
57
|
+
"semantic-release": "^25.0.3",
|
|
58
58
|
"ts-node": "^10.9.2",
|
|
59
|
-
"turbo": "^2.
|
|
59
|
+
"turbo": "^2.8.8",
|
|
60
60
|
"typescript": "^5.9.3"
|
|
61
61
|
},
|
|
62
62
|
"workspaces": [
|
|
@@ -71,11 +71,15 @@
|
|
|
71
71
|
"@types/react": "19.2.7",
|
|
72
72
|
"@types/react-dom": "19.2.3",
|
|
73
73
|
"sharp": "^0.33.5",
|
|
74
|
-
"@nestjs/common": "^11.1.
|
|
75
|
-
"@nestjs/
|
|
76
|
-
"@nestjs/
|
|
77
|
-
"@nestjs/platform-
|
|
78
|
-
"@nestjs/
|
|
74
|
+
"@nestjs/common": "^11.1.13",
|
|
75
|
+
"@nestjs/config": "^4.0.3",
|
|
76
|
+
"@nestjs/core": "^11.1.13",
|
|
77
|
+
"@nestjs/platform-fastify": "^11.1.13",
|
|
78
|
+
"@nestjs/platform-socket.io": "^11.1.13",
|
|
79
|
+
"@nestjs/websockets": "^11.1.13",
|
|
80
|
+
"react": "19.2.4",
|
|
81
|
+
"react-dom": "19.2.4",
|
|
82
|
+
"jotai": "^2.17.1"
|
|
79
83
|
},
|
|
80
84
|
"ignoredBuiltDependencies": [
|
|
81
85
|
"@tensorflow/tfjs-node",
|