create-cundi-app 1.0.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/README.md +70 -0
- package/index.js +138 -0
- package/package.json +37 -0
- package/template/.env.example +8 -0
- package/template/README.md +54 -0
- package/template/_gitignore +27 -0
- package/template/index.html +17 -0
- package/template/package.json.template +52 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/manifest.json +15 -0
- package/template/src/App.tsx +192 -0
- package/template/src/accessControlProvider.ts +41 -0
- package/template/src/contexts/color-mode/index.tsx +2 -0
- package/template/src/i18n.ts +29 -0
- package/template/src/index.css +5 -0
- package/template/src/index.tsx +14 -0
- package/template/src/pages/dashboard.tsx +73 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.json +31 -0
- package/template/tsconfig.node.json +10 -0
- package/template/vite.config.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# create-cundi-app
|
|
2
|
+
|
|
3
|
+
Create a new Cundi app with React + Refine + Ant Design.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Using npm
|
|
9
|
+
npm create cundi-app@latest my-app
|
|
10
|
+
|
|
11
|
+
# Using npx
|
|
12
|
+
npx create-cundi-app my-app
|
|
13
|
+
|
|
14
|
+
# Using yarn
|
|
15
|
+
yarn create cundi-app my-app
|
|
16
|
+
|
|
17
|
+
# Using pnpm
|
|
18
|
+
pnpm create cundi-app my-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What's Included
|
|
22
|
+
|
|
23
|
+
The generated project includes:
|
|
24
|
+
|
|
25
|
+
- **Framework**: React 19 + TypeScript
|
|
26
|
+
- **Build Tool**: Vite
|
|
27
|
+
- **UI Library**: Ant Design
|
|
28
|
+
- **Metasystem**: [Refine](https://refine.dev/)
|
|
29
|
+
- **Core SDK**: `@cundi/refine-xaf`
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
- 🔐 Authentication (Local + Keycloak SSO)
|
|
34
|
+
- 👤 User Management
|
|
35
|
+
- 🛡️ Role-based Access Control
|
|
36
|
+
- 🌍 Internationalization (i18n)
|
|
37
|
+
- 🌙 Dark Mode Support
|
|
38
|
+
- 📊 Dashboard Template
|
|
39
|
+
|
|
40
|
+
## After Creating
|
|
41
|
+
|
|
42
|
+
1. Navigate to your project:
|
|
43
|
+
```bash
|
|
44
|
+
cd my-app
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. Install dependencies:
|
|
48
|
+
```bash
|
|
49
|
+
npm install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. Configure environment:
|
|
53
|
+
```bash
|
|
54
|
+
cp .env.example .env
|
|
55
|
+
# Edit .env with your API URL and Keycloak settings
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
4. Start development server:
|
|
59
|
+
```bash
|
|
60
|
+
npm run dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
- [Refine Documentation](https://refine.dev/docs/)
|
|
66
|
+
- [@cundi/refine-xaf SDK](https://www.npmjs.com/package/@cundi/refine-xaf)
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import kleur from "kleur";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Main CLI entry point
|
|
14
|
+
*/
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(kleur.cyan().bold("🚀 Create Cundi App"));
|
|
18
|
+
console.log(kleur.gray("────────────────────────────────────"));
|
|
19
|
+
console.log();
|
|
20
|
+
|
|
21
|
+
// Get project name from command line or prompt
|
|
22
|
+
let projectName = process.argv[2];
|
|
23
|
+
|
|
24
|
+
if (!projectName) {
|
|
25
|
+
const response = await prompts({
|
|
26
|
+
type: "text",
|
|
27
|
+
name: "projectName",
|
|
28
|
+
message: "Project name:",
|
|
29
|
+
initial: "my-cundi-app",
|
|
30
|
+
validate: (value) => {
|
|
31
|
+
if (!value) return "Project name is required";
|
|
32
|
+
if (!/^[a-z0-9-_]+$/i.test(value)) {
|
|
33
|
+
return "Project name can only contain letters, numbers, hyphens and underscores";
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.projectName) {
|
|
40
|
+
console.log(kleur.red("✖ Operation cancelled"));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
projectName = response.projectName;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
48
|
+
|
|
49
|
+
// Check if directory already exists
|
|
50
|
+
if (fs.existsSync(targetDir)) {
|
|
51
|
+
const { overwrite } = await prompts({
|
|
52
|
+
type: "confirm",
|
|
53
|
+
name: "overwrite",
|
|
54
|
+
message: `Directory "${projectName}" already exists. Overwrite?`,
|
|
55
|
+
initial: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!overwrite) {
|
|
59
|
+
console.log(kleur.red("✖ Operation cancelled"));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await fs.emptyDir(targetDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(kleur.blue(`Creating project in ${kleur.bold(targetDir)}...`));
|
|
68
|
+
console.log();
|
|
69
|
+
|
|
70
|
+
// Copy template files
|
|
71
|
+
const templateDir = path.join(__dirname, "template");
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await fs.copy(templateDir, targetDir, {
|
|
75
|
+
filter: (src) => {
|
|
76
|
+
// Skip node_modules and dist if they exist
|
|
77
|
+
const relativePath = path.relative(templateDir, src);
|
|
78
|
+
return !relativePath.includes("node_modules") && !relativePath.includes("dist");
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Generate package.json with project name
|
|
83
|
+
const pkgTemplatePath = path.join(targetDir, "package.json.template");
|
|
84
|
+
const pkgTargetPath = path.join(targetDir, "package.json");
|
|
85
|
+
|
|
86
|
+
if (fs.existsSync(pkgTemplatePath)) {
|
|
87
|
+
let pkgContent = await fs.readFile(pkgTemplatePath, "utf-8");
|
|
88
|
+
pkgContent = pkgContent.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
89
|
+
await fs.writeFile(pkgTargetPath, pkgContent);
|
|
90
|
+
await fs.remove(pkgTemplatePath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Rename .env.example to .env.example (ensure it exists)
|
|
94
|
+
const envExamplePath = path.join(targetDir, ".env.example");
|
|
95
|
+
if (!fs.existsSync(envExamplePath)) {
|
|
96
|
+
await fs.writeFile(
|
|
97
|
+
envExamplePath,
|
|
98
|
+
`# API Configuration
|
|
99
|
+
VITE_API_URL=http://localhost:5000/api
|
|
100
|
+
|
|
101
|
+
# Keycloak Configuration (Optional)
|
|
102
|
+
VITE_KEYCLOAK_URL=http://localhost:8080
|
|
103
|
+
VITE_KEYCLOAK_REALM=your-realm
|
|
104
|
+
VITE_KEYCLOAK_CLIENT_ID=your-client-id
|
|
105
|
+
VITE_REDIRECT_URI=http://localhost:5173/auth/callback
|
|
106
|
+
`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Rename _gitignore to .gitignore (npm ignores .gitignore in packages)
|
|
111
|
+
const gitignoreSrc = path.join(targetDir, "_gitignore");
|
|
112
|
+
const gitignoreDest = path.join(targetDir, ".gitignore");
|
|
113
|
+
if (fs.existsSync(gitignoreSrc)) {
|
|
114
|
+
await fs.rename(gitignoreSrc, gitignoreDest);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(kleur.green("✔ Project created successfully!"));
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(kleur.cyan("Next steps:"));
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(` ${kleur.gray("$")} cd ${projectName}`);
|
|
122
|
+
console.log(` ${kleur.gray("$")} npm install`);
|
|
123
|
+
console.log(` ${kleur.gray("$")} cp .env.example .env ${kleur.gray("# Configure your environment")}`);
|
|
124
|
+
console.log(` ${kleur.gray("$")} npm run dev`);
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(kleur.gray("────────────────────────────────────"));
|
|
127
|
+
console.log(kleur.blue("Happy coding! 🎉"));
|
|
128
|
+
console.log();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(kleur.red("✖ Error creating project:"), error);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main().catch((error) => {
|
|
136
|
+
console.error(error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-cundi-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create a new Cundi app with React + Refine + Ant Design",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-cundi-app": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"create",
|
|
15
|
+
"cundi",
|
|
16
|
+
"refine",
|
|
17
|
+
"react",
|
|
18
|
+
"antd",
|
|
19
|
+
"xaf",
|
|
20
|
+
"scaffold",
|
|
21
|
+
"template"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/antonylu0826/Cundi"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"prompts": "^2.4.2",
|
|
31
|
+
"kleur": "^4.1.5",
|
|
32
|
+
"fs-extra": "^11.2.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# API Configuration
|
|
2
|
+
VITE_API_URL=http://localhost:5000/api
|
|
3
|
+
|
|
4
|
+
# Keycloak Configuration (Optional)
|
|
5
|
+
VITE_KEYCLOAK_URL=http://localhost:8080
|
|
6
|
+
VITE_KEYCLOAK_REALM=your-realm
|
|
7
|
+
VITE_KEYCLOAK_CLIENT_ID=your-client-id
|
|
8
|
+
VITE_REDIRECT_URI=http://localhost:5173/auth/callback
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Cundi App
|
|
2
|
+
|
|
3
|
+
Built with React + Refine + Ant Design.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
### 1. Install Dependencies
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Configure Environment
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp .env.example .env
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Edit `.env` with your settings:
|
|
20
|
+
|
|
21
|
+
```env
|
|
22
|
+
VITE_API_URL=http://localhost:5000/api
|
|
23
|
+
|
|
24
|
+
# Keycloak Configuration (Optional)
|
|
25
|
+
VITE_KEYCLOAK_URL=http://localhost:8080
|
|
26
|
+
VITE_KEYCLOAK_REALM=your-realm
|
|
27
|
+
VITE_KEYCLOAK_CLIENT_ID=your-client-id
|
|
28
|
+
VITE_REDIRECT_URI=http://localhost:5173/auth/callback
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 3. Start Development Server
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The application will launch at `http://localhost:5173`.
|
|
38
|
+
|
|
39
|
+
## Login
|
|
40
|
+
|
|
41
|
+
| Route | Description |
|
|
42
|
+
|-------|-------------|
|
|
43
|
+
| `/login` | Keycloak SSO login (default) |
|
|
44
|
+
| `/login/api` | Local account login |
|
|
45
|
+
|
|
46
|
+
### Default Local Account
|
|
47
|
+
|
|
48
|
+
- **Username**: Admin
|
|
49
|
+
- **Password**: Admin123
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
- [Refine Documentation](https://refine.dev/docs/)
|
|
54
|
+
- [@cundi/refine-xaf SDK](https://www.npmjs.com/package/@cundi/refine-xaf)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
|
|
7
|
+
# Environment files
|
|
8
|
+
.env
|
|
9
|
+
.env.local
|
|
10
|
+
.env.*.local
|
|
11
|
+
|
|
12
|
+
# IDE
|
|
13
|
+
.idea/
|
|
14
|
+
.vscode/
|
|
15
|
+
*.swp
|
|
16
|
+
*.swo
|
|
17
|
+
|
|
18
|
+
# OS
|
|
19
|
+
.DS_Store
|
|
20
|
+
Thumbs.db
|
|
21
|
+
|
|
22
|
+
# Logs
|
|
23
|
+
*.log
|
|
24
|
+
npm-debug.log*
|
|
25
|
+
|
|
26
|
+
# TypeScript
|
|
27
|
+
*.tsbuildinfo
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<link rel="icon" href="/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta name="theme-color" content="#000000" />
|
|
8
|
+
<meta name="description" content="Cundi App - Built with React + Refine + Ant Design" />
|
|
9
|
+
<link rel="manifest" href="/manifest.json" />
|
|
10
|
+
<title>Cundi App</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc && refine build",
|
|
8
|
+
"dev": "refine dev",
|
|
9
|
+
"refine": "refine",
|
|
10
|
+
"start": "refine start"
|
|
11
|
+
},
|
|
12
|
+
"browserslist": {
|
|
13
|
+
"production": [
|
|
14
|
+
">0.2%",
|
|
15
|
+
"not dead",
|
|
16
|
+
"not op_mini all"
|
|
17
|
+
],
|
|
18
|
+
"development": [
|
|
19
|
+
"last 1 chrome version",
|
|
20
|
+
"last 1 firefox version",
|
|
21
|
+
"last 1 safari version"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@ant-design/icons": "^5.5.1",
|
|
26
|
+
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
|
27
|
+
"@cundi/refine-xaf": "^1.0.3",
|
|
28
|
+
"@refinedev/antd": "^6.0.3",
|
|
29
|
+
"@refinedev/cli": "^2.16.50",
|
|
30
|
+
"@refinedev/core": "^5.0.6",
|
|
31
|
+
"@refinedev/react-router": "^2.0.3",
|
|
32
|
+
"@refinedev/simple-rest": "^6.0.1",
|
|
33
|
+
"antd": "^5.23.0",
|
|
34
|
+
"i18next": "^25.7.3",
|
|
35
|
+
"i18next-browser-languagedetector": "^8.2.0",
|
|
36
|
+
"react": "^19.1.0",
|
|
37
|
+
"react-dom": "^19.1.0",
|
|
38
|
+
"react-i18next": "^16.5.0",
|
|
39
|
+
"react-router": "^7.0.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20",
|
|
43
|
+
"@types/react": "^19.1.0",
|
|
44
|
+
"@types/react-dom": "^19.1.0",
|
|
45
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
46
|
+
"typescript": "^5.8.3",
|
|
47
|
+
"vite": "^5.4.15"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"short_name": "Cundi App",
|
|
3
|
+
"name": "Cundi App",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"start_url": ".",
|
|
12
|
+
"display": "standalone",
|
|
13
|
+
"theme_color": "#1677ff",
|
|
14
|
+
"background_color": "#ffffff"
|
|
15
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Refine,
|
|
3
|
+
Authenticated,
|
|
4
|
+
} from "@refinedev/core";
|
|
5
|
+
import {
|
|
6
|
+
useNotificationProvider,
|
|
7
|
+
ThemedLayout,
|
|
8
|
+
ErrorComponent,
|
|
9
|
+
RefineThemes,
|
|
10
|
+
} from "@refinedev/antd";
|
|
11
|
+
import {
|
|
12
|
+
DashboardOutlined,
|
|
13
|
+
UserOutlined,
|
|
14
|
+
SettingOutlined,
|
|
15
|
+
TeamOutlined,
|
|
16
|
+
} from "@ant-design/icons";
|
|
17
|
+
|
|
18
|
+
import routerProvider, {
|
|
19
|
+
NavigateToResource,
|
|
20
|
+
CatchAllNavigate,
|
|
21
|
+
UnsavedChangesNotifier,
|
|
22
|
+
DocumentTitleHandler,
|
|
23
|
+
} from "@refinedev/react-router";
|
|
24
|
+
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
|
|
25
|
+
import { App as AntdApp, ConfigProvider, theme } from "antd";
|
|
26
|
+
import { useTranslation } from "react-i18next";
|
|
27
|
+
import "./i18n";
|
|
28
|
+
|
|
29
|
+
import "@ant-design/v5-patch-for-react-19";
|
|
30
|
+
import "@refinedev/antd/dist/reset.css";
|
|
31
|
+
|
|
32
|
+
import { DashboardPage } from "./pages/dashboard";
|
|
33
|
+
import {
|
|
34
|
+
authProvider,
|
|
35
|
+
dataProvider,
|
|
36
|
+
Header,
|
|
37
|
+
LoginPage,
|
|
38
|
+
KeycloakLoginPage,
|
|
39
|
+
AuthCallback,
|
|
40
|
+
ApplicationUserList,
|
|
41
|
+
ApplicationUserCreate,
|
|
42
|
+
ApplicationUserEdit,
|
|
43
|
+
RoleList,
|
|
44
|
+
RoleCreate,
|
|
45
|
+
RoleEdit,
|
|
46
|
+
} from "@cundi/refine-xaf";
|
|
47
|
+
|
|
48
|
+
import { ColorModeContextProvider, useColorMode } from "./contexts/color-mode";
|
|
49
|
+
import { accessControlProvider } from "./accessControlProvider";
|
|
50
|
+
|
|
51
|
+
const API_URL = import.meta.env.VITE_API_URL + "/odata";
|
|
52
|
+
|
|
53
|
+
const InnerApp: React.FC = () => {
|
|
54
|
+
const { mode } = useColorMode();
|
|
55
|
+
const { t, i18n } = useTranslation();
|
|
56
|
+
|
|
57
|
+
const i18nProvider = {
|
|
58
|
+
translate: (key: string, params: any) => t(key, params) as string,
|
|
59
|
+
changeLocale: (lang: string) => i18n.changeLanguage(lang),
|
|
60
|
+
getLocale: () => i18n.language,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<BrowserRouter>
|
|
65
|
+
<ConfigProvider
|
|
66
|
+
theme={{
|
|
67
|
+
...RefineThemes.Blue,
|
|
68
|
+
algorithm: mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<AntdApp>
|
|
72
|
+
<Refine
|
|
73
|
+
authProvider={authProvider}
|
|
74
|
+
accessControlProvider={accessControlProvider}
|
|
75
|
+
dataProvider={dataProvider(API_URL)}
|
|
76
|
+
i18nProvider={i18nProvider}
|
|
77
|
+
routerProvider={routerProvider}
|
|
78
|
+
resources={[
|
|
79
|
+
{
|
|
80
|
+
name: "dashboard",
|
|
81
|
+
list: "/",
|
|
82
|
+
meta: {
|
|
83
|
+
label: t("sider.dashboard"),
|
|
84
|
+
icon: <DashboardOutlined />,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "ApplicationUser",
|
|
89
|
+
list: "/ApplicationUsers",
|
|
90
|
+
create: "/ApplicationUsers/create",
|
|
91
|
+
edit: "/ApplicationUsers/edit/:id",
|
|
92
|
+
meta: {
|
|
93
|
+
label: t("sider.users"),
|
|
94
|
+
icon: <UserOutlined />,
|
|
95
|
+
parent: t("sider.settings"),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "PermissionPolicyRole",
|
|
100
|
+
list: "/PermissionPolicyRoles",
|
|
101
|
+
create: "/PermissionPolicyRoles/create",
|
|
102
|
+
edit: "/PermissionPolicyRoles/edit/:id",
|
|
103
|
+
meta: {
|
|
104
|
+
label: t("sider.roles"),
|
|
105
|
+
parent: t("sider.settings"),
|
|
106
|
+
icon: <TeamOutlined />,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "Settings",
|
|
111
|
+
meta: {
|
|
112
|
+
label: t("sider.settings"),
|
|
113
|
+
icon: <SettingOutlined />,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
]}
|
|
117
|
+
notificationProvider={useNotificationProvider}
|
|
118
|
+
options={{
|
|
119
|
+
syncWithLocation: true,
|
|
120
|
+
warnWhenUnsavedChanges: true,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<Routes>
|
|
124
|
+
<Route
|
|
125
|
+
element={
|
|
126
|
+
<Authenticated
|
|
127
|
+
key="authenticated-routes"
|
|
128
|
+
fallback={<CatchAllNavigate to="/login" />}
|
|
129
|
+
>
|
|
130
|
+
<ThemedLayout Header={Header}>
|
|
131
|
+
<Outlet />
|
|
132
|
+
</ThemedLayout>
|
|
133
|
+
</Authenticated>
|
|
134
|
+
}
|
|
135
|
+
>
|
|
136
|
+
<Route index element={<DashboardPage />} />
|
|
137
|
+
|
|
138
|
+
<Route path="/ApplicationUsers">
|
|
139
|
+
<Route index element={<ApplicationUserList />} />
|
|
140
|
+
<Route path="create" element={<ApplicationUserCreate />} />
|
|
141
|
+
<Route path="edit/:id" element={<ApplicationUserEdit />} />
|
|
142
|
+
</Route>
|
|
143
|
+
|
|
144
|
+
<Route path="/PermissionPolicyRoles">
|
|
145
|
+
<Route index element={<RoleList />} />
|
|
146
|
+
<Route path="create" element={<RoleCreate />} />
|
|
147
|
+
<Route path="edit/:id" element={<RoleEdit />} />
|
|
148
|
+
</Route>
|
|
149
|
+
</Route>
|
|
150
|
+
|
|
151
|
+
<Route
|
|
152
|
+
element={
|
|
153
|
+
<Authenticated key="auth-pages" fallback={<Outlet />}>
|
|
154
|
+
<NavigateToResource resource="dashboard" />
|
|
155
|
+
</Authenticated>
|
|
156
|
+
}
|
|
157
|
+
>
|
|
158
|
+
<Route path="/login" element={<KeycloakLoginPage />} />
|
|
159
|
+
<Route path="/login/api" element={<LoginPage />} />
|
|
160
|
+
<Route path="/auth/callback" element={<AuthCallback />} />
|
|
161
|
+
</Route>
|
|
162
|
+
|
|
163
|
+
<Route
|
|
164
|
+
element={
|
|
165
|
+
<Authenticated key="catch-all">
|
|
166
|
+
<ThemedLayout Header={Header}>
|
|
167
|
+
<Outlet />
|
|
168
|
+
</ThemedLayout>
|
|
169
|
+
</Authenticated>
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
<Route path="*" element={<ErrorComponent />} />
|
|
173
|
+
</Route>
|
|
174
|
+
</Routes>
|
|
175
|
+
<UnsavedChangesNotifier />
|
|
176
|
+
<DocumentTitleHandler />
|
|
177
|
+
</Refine>
|
|
178
|
+
</AntdApp>
|
|
179
|
+
</ConfigProvider>
|
|
180
|
+
</BrowserRouter>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const App: React.FC = () => {
|
|
185
|
+
return (
|
|
186
|
+
<ColorModeContextProvider>
|
|
187
|
+
<InnerApp />
|
|
188
|
+
</ColorModeContextProvider>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default App;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AccessControlProvider } from "@refinedev/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Protected resources that require admin access
|
|
5
|
+
*/
|
|
6
|
+
const ADMIN_ONLY_RESOURCES = ["ApplicationUser", "PermissionPolicyRole", "Settings"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Access control provider with role-based permissions
|
|
10
|
+
* Currently supports Admin/non-Admin model with action-level control
|
|
11
|
+
*/
|
|
12
|
+
export const accessControlProvider: AccessControlProvider = {
|
|
13
|
+
can: async ({ resource, action, params }) => {
|
|
14
|
+
const isAdmin = localStorage.getItem("user_is_admin") === "true";
|
|
15
|
+
const roles = JSON.parse(localStorage.getItem("user_roles") || "[]") as string[];
|
|
16
|
+
|
|
17
|
+
// Admin-only resources check
|
|
18
|
+
if (resource && ADMIN_ONLY_RESOURCES.includes(resource) && !isAdmin) {
|
|
19
|
+
return {
|
|
20
|
+
can: false,
|
|
21
|
+
reason: "Only Administrators can access this resource",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Delete action requires admin privileges for all resources
|
|
26
|
+
if (action === "delete" && !isAdmin) {
|
|
27
|
+
return {
|
|
28
|
+
can: false,
|
|
29
|
+
reason: "Only Administrators can delete records",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { can: true };
|
|
34
|
+
},
|
|
35
|
+
options: {
|
|
36
|
+
buttons: {
|
|
37
|
+
enableAccessControl: true,
|
|
38
|
+
hideIfUnauthorized: true,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import i18n from "i18next";
|
|
2
|
+
import { initReactI18next } from "react-i18next";
|
|
3
|
+
import LanguageDetector from "i18next-browser-languagedetector";
|
|
4
|
+
import { refineXafTranslations } from "@cundi/refine-xaf";
|
|
5
|
+
|
|
6
|
+
// Use translations from the SDK
|
|
7
|
+
const { en, "zh-TW": zhTW } = refineXafTranslations;
|
|
8
|
+
|
|
9
|
+
i18n
|
|
10
|
+
.use(LanguageDetector)
|
|
11
|
+
.use(initReactI18next)
|
|
12
|
+
.init({
|
|
13
|
+
resources: {
|
|
14
|
+
en: {
|
|
15
|
+
translation: en,
|
|
16
|
+
},
|
|
17
|
+
"zh-TW": {
|
|
18
|
+
translation: zhTW,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
lng: "zh-TW", // Default language
|
|
22
|
+
fallbackLng: "en",
|
|
23
|
+
debug: false,
|
|
24
|
+
interpolation: {
|
|
25
|
+
escapeValue: false,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export default i18n;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
|
|
4
|
+
import App from "./App";
|
|
5
|
+
|
|
6
|
+
import "./index.css";
|
|
7
|
+
|
|
8
|
+
const container = document.getElementById("root");
|
|
9
|
+
const root = createRoot(container!);
|
|
10
|
+
root.render(
|
|
11
|
+
<React.StrictMode>
|
|
12
|
+
<App />
|
|
13
|
+
</React.StrictMode>
|
|
14
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useGetIdentity } from "@refinedev/core";
|
|
2
|
+
import { Row, Col, Card, Avatar, Typography, Space } from "antd";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
|
|
5
|
+
const { Text, Title } = Typography;
|
|
6
|
+
|
|
7
|
+
export const DashboardPage: React.FC = () => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const { data: identity } = useGetIdentity<{
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
avatar: string;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div style={{ padding: "24px" }}>
|
|
17
|
+
<Title level={2}>{t("sider.dashboard")}</Title>
|
|
18
|
+
<Row gutter={[24, 24]}>
|
|
19
|
+
<Col xs={24} sm={12} lg={8}>
|
|
20
|
+
<Card
|
|
21
|
+
title={t("dashboard.welcome")}
|
|
22
|
+
style={{ borderRadius: "12px" }}
|
|
23
|
+
>
|
|
24
|
+
<Space align="center">
|
|
25
|
+
<Avatar size={64} src={identity?.avatar}>
|
|
26
|
+
{identity?.name?.charAt(0)}
|
|
27
|
+
</Avatar>
|
|
28
|
+
<div>
|
|
29
|
+
<Text strong style={{ fontSize: "18px" }}>
|
|
30
|
+
{identity?.name}
|
|
31
|
+
</Text>
|
|
32
|
+
<br />
|
|
33
|
+
<Text type="secondary">{t("dashboard.welcomeMessage")}</Text>
|
|
34
|
+
</div>
|
|
35
|
+
</Space>
|
|
36
|
+
</Card>
|
|
37
|
+
</Col>
|
|
38
|
+
|
|
39
|
+
<Col xs={24} sm={12} lg={8}>
|
|
40
|
+
<Card
|
|
41
|
+
title={t("dashboard.quickStart")}
|
|
42
|
+
style={{ borderRadius: "12px" }}
|
|
43
|
+
>
|
|
44
|
+
<ul style={{ paddingLeft: "20px", margin: 0 }}>
|
|
45
|
+
<li>{t("dashboard.tip1")}</li>
|
|
46
|
+
<li>{t("dashboard.tip2")}</li>
|
|
47
|
+
<li>{t("dashboard.tip3")}</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</Card>
|
|
50
|
+
</Col>
|
|
51
|
+
|
|
52
|
+
<Col xs={24} sm={12} lg={8}>
|
|
53
|
+
<Card
|
|
54
|
+
title={t("dashboard.resources")}
|
|
55
|
+
style={{ borderRadius: "12px" }}
|
|
56
|
+
>
|
|
57
|
+
<Space direction="vertical">
|
|
58
|
+
<a href="https://refine.dev/docs/" target="_blank" rel="noopener noreferrer">
|
|
59
|
+
Refine Documentation
|
|
60
|
+
</a>
|
|
61
|
+
<a href="https://www.npmjs.com/package/@cundi/refine-xaf" target="_blank" rel="noopener noreferrer">
|
|
62
|
+
@cundi/refine-xaf SDK
|
|
63
|
+
</a>
|
|
64
|
+
<a href="https://ant.design/components/overview" target="_blank" rel="noopener noreferrer">
|
|
65
|
+
Ant Design Components
|
|
66
|
+
</a>
|
|
67
|
+
</Space>
|
|
68
|
+
</Card>
|
|
69
|
+
</Col>
|
|
70
|
+
</Row>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": [
|
|
6
|
+
"DOM",
|
|
7
|
+
"DOM.Iterable",
|
|
8
|
+
"ESNext"
|
|
9
|
+
],
|
|
10
|
+
"allowJs": false,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": false,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"module": "ESNext",
|
|
17
|
+
"moduleResolution": "Node",
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
"jsx": "react-jsx"
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"references": [
|
|
27
|
+
{
|
|
28
|
+
"path": "./tsconfig.node.json"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
resolve: {
|
|
7
|
+
dedupe: ["react", "react-dom", "antd", "@refinedev/core", "@refinedev/antd", "@tanstack/react-query"],
|
|
8
|
+
},
|
|
9
|
+
});
|