robodev 0.47.2 → 0.48.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 +3 -3
- package/src/emit-starter.test.ts +6 -3
- package/templates/auth-chat/ROBODEV.md +7 -1
- package/templates/auth-chat/package.json +2 -2
- package/templates/backend/ROBODEV.md +7 -1
- package/templates/backend/package.json +2 -2
- package/templates/catalog.json +5 -0
- package/templates/empty/ROBODEV.md +7 -1
- package/templates/empty/package.json +2 -2
- package/templates/marketplace/ROBODEV.md +7 -1
- package/templates/marketplace/package.json +2 -2
- package/templates/povio/.config/local.spa.template.yml +27 -0
- package/templates/povio/.oxlint-base.json +29 -0
- package/templates/povio/.oxlintrc.json +78 -0
- package/templates/povio/.rulesync/rules/frontend-api-boundary.md +36 -0
- package/templates/povio/.rulesync/rules/frontend-component-structure.md +40 -0
- package/templates/povio/.rulesync/rules/frontend-forms.md +27 -0
- package/templates/povio/.rulesync/rules/frontend-notifications.md +24 -0
- package/templates/povio/.rulesync/rules/frontend-povio-components.md +42 -0
- package/templates/povio/.rulesync/rules/frontend-povio-ui.md +45 -0
- package/templates/povio/.rulesync/rules/frontend-query-autocomplete.md +24 -0
- package/templates/povio/.rulesync/rules/frontend-tables-and-lists.md +26 -0
- package/templates/povio/.rulesync/rules/frontend-translations.md +25 -0
- package/templates/povio/.rulesync/rules/project-overview.md +32 -0
- package/templates/povio/.rulesync/rules/robodev-api.md +30 -0
- package/templates/povio/.rulesync/rules/robodev-auth.md +40 -0
- package/templates/povio/.rulesync/rules/robodev-database.md +22 -0
- package/templates/povio/.rulesync/rules/role-based-app-structure.md +36 -0
- package/templates/povio/.rulesync/skills/media-feature/SKILL.md +21 -0
- package/templates/povio/.rulesync/skills/povio-ui-styling/SKILL.md +211 -0
- package/templates/povio/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
- package/templates/povio/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
- package/templates/povio/.rulesync/skills/robodev-table/SKILL.md +12 -0
- package/templates/povio/README.md +40 -0
- package/templates/povio/ROBODEV.md +84 -0
- package/templates/povio/apps/fe/index.html +24 -0
- package/templates/povio/apps/fe/openapi-codegen.config.ts +72 -0
- package/templates/povio/apps/fe/package.json +71 -0
- package/templates/povio/apps/fe/public/apple-touch-icon.png +0 -0
- package/templates/povio/apps/fe/public/favicon-96x96.png +0 -0
- package/templates/povio/apps/fe/public/favicon.ico +0 -0
- package/templates/povio/apps/fe/public/favicon.svg +3 -0
- package/templates/povio/apps/fe/public/site.webmanifest +21 -0
- package/templates/povio/apps/fe/public/web-app-manifest-192x192.png +0 -0
- package/templates/povio/apps/fe/public/web-app-manifest-512x512.png +0 -0
- package/templates/povio/apps/fe/src/assets/locales/en/translation.json +166 -0
- package/templates/povio/apps/fe/src/clients/app-rest-client.ts +15 -0
- package/templates/povio/apps/fe/src/clients/auth-token-store.ts +101 -0
- package/templates/povio/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
- package/templates/povio/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
- package/templates/povio/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +45 -0
- package/templates/povio/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
- package/templates/povio/apps/fe/src/components/404.tsx +18 -0
- package/templates/povio/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
- package/templates/povio/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
- package/templates/povio/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
- package/templates/povio/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
- package/templates/povio/apps/fe/src/components/features/home/HomePage.tsx +21 -0
- package/templates/povio/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
- package/templates/povio/apps/fe/src/components/layout/AppLayout.tsx +19 -0
- package/templates/povio/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
- package/templates/povio/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
- package/templates/povio/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
- package/templates/povio/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
- package/templates/povio/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
- package/templates/povio/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
- package/templates/povio/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
- package/templates/povio/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
- package/templates/povio/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
- package/templates/povio/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
- package/templates/povio/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
- package/templates/povio/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
- package/templates/povio/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
- package/templates/povio/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
- package/templates/povio/apps/fe/src/components/shared/ui/Card.tsx +60 -0
- package/templates/povio/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
- package/templates/povio/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
- package/templates/povio/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
- package/templates/povio/apps/fe/src/config/app.config.ts +23 -0
- package/templates/povio/apps/fe/src/config/i18n.ts +37 -0
- package/templates/povio/apps/fe/src/config/inits/logger.ts +7 -0
- package/templates/povio/apps/fe/src/config/jwt.config.ts +2 -0
- package/templates/povio/apps/fe/src/config/query.config.ts +20 -0
- package/templates/povio/apps/fe/src/hooks/useAuth.ts +5 -0
- package/templates/povio/apps/fe/src/main.tsx +52 -0
- package/templates/povio/apps/fe/src/pages/(guest)/login.tsx +23 -0
- package/templates/povio/apps/fe/src/pages/(guest)/register.tsx +23 -0
- package/templates/povio/apps/fe/src/pages/(guest)/route.tsx +18 -0
- package/templates/povio/apps/fe/src/pages/(private)/index.tsx +23 -0
- package/templates/povio/apps/fe/src/pages/(private)/profile.tsx +23 -0
- package/templates/povio/apps/fe/src/pages/(private)/route.tsx +18 -0
- package/templates/povio/apps/fe/src/pages/(public)/auth.tsx +37 -0
- package/templates/povio/apps/fe/src/pages/(public)/route.tsx +9 -0
- package/templates/povio/apps/fe/src/pages/__root.tsx +133 -0
- package/templates/povio/apps/fe/src/providers/AppErrorBoundary.tsx +44 -0
- package/templates/povio/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
- package/templates/povio/apps/fe/src/providers/index.tsx +44 -0
- package/templates/povio/apps/fe/src/providers/jwt.provider.tsx +95 -0
- package/templates/povio/apps/fe/src/routeTree.gen.ts +225 -0
- package/templates/povio/apps/fe/src/styles/base.css +84 -0
- package/templates/povio/apps/fe/src/styles/fonts/fonts.tsx +10 -0
- package/templates/povio/apps/fe/src/styles/globals.css +26 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/modal.override.ts +62 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
- package/templates/povio/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
- package/templates/povio/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
- package/templates/povio/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
- package/templates/povio/apps/fe/src/styles/theme.css +2177 -0
- package/templates/povio/apps/fe/src/types/i18next.d.ts +12 -0
- package/templates/povio/apps/fe/src/types/table.d.ts +17 -0
- package/templates/povio/apps/fe/src/types/ui.d.ts +26 -0
- package/templates/povio/apps/fe/src/types/vite-env.d.ts +13 -0
- package/templates/povio/apps/fe/src/utils/date.utils.ts +17 -0
- package/templates/povio/apps/fe/src/utils/image-fallback.ts +9 -0
- package/templates/povio/apps/fe/src/utils/number.utils.ts +12 -0
- package/templates/povio/apps/fe/src/utils/string.utils.ts +5 -0
- package/templates/povio/apps/fe/src/vite-env.d.ts +1 -0
- package/templates/povio/apps/fe/tsconfig.app.json +32 -0
- package/templates/povio/apps/fe/tsconfig.json +9 -0
- package/templates/povio/apps/fe/tsconfig.node.json +31 -0
- package/templates/povio/apps/fe/vite.config.ts +150 -0
- package/templates/povio/openapi.json +911 -0
- package/templates/povio/oxfmt.config.js +23 -0
- package/templates/povio/package.json +19 -0
- package/templates/povio/robodev/api/_lib.ts +99 -0
- package/templates/povio/robodev/api/health.ts +7 -0
- package/templates/povio/robodev/database.ts +28 -0
- package/templates/povio/robodev/package.json +4 -0
- package/templates/povio/rulesync.jsonc +18 -0
- package/templates/povio/tsconfig.json +11 -0
- package/templates/shadcn/.rulesync/rules/frontend-api-boundary.md +29 -0
- package/templates/shadcn/.rulesync/rules/frontend-component-structure.md +19 -0
- package/templates/shadcn/.rulesync/rules/frontend-hosted-ui.md +38 -0
- package/templates/shadcn/.rulesync/rules/project-overview.md +29 -0
- package/templates/shadcn/.rulesync/rules/robodev-api.md +22 -0
- package/templates/shadcn/.rulesync/rules/robodev-auth.md +47 -0
- package/templates/shadcn/.rulesync/rules/robodev-database.md +21 -0
- package/templates/shadcn/.rulesync/skills/robodev-crud-page/SKILL.md +181 -0
- package/templates/shadcn/README.md +9 -4
- package/templates/shadcn/ROBODEV.md +14 -6
- package/templates/shadcn/package.json +4 -6
- package/templates/shadcn/rulesync.jsonc +18 -0
- package/templates/shadcn/src/App.tsx +12 -3
- package/templates/shadcn/src/app.css +359 -0
- package/templates/shadcn/src/components/auth/AuthCard.tsx +29 -0
- package/templates/shadcn/src/components/ui/badge.tsx +17 -0
- package/templates/shadcn/src/components/ui/card.tsx +28 -0
- package/templates/shadcn/src/components/ui/dialog.tsx +38 -0
- package/templates/shadcn/src/components/ui/empty-state.tsx +17 -0
- package/templates/shadcn/src/components/ui/input.tsx +8 -0
- package/templates/shadcn/src/components/ui/label.tsx +8 -0
- package/templates/shadcn/src/components/ui/skeleton.tsx +6 -0
- package/templates/shadcn/src/components/ui/spinner.tsx +3 -0
- package/templates/shadcn/src/components/ui/table.tsx +30 -0
- package/templates/shadcn/src/components/ui/textarea.tsx +8 -0
- package/templates/shadcn/src/lib/auth.tsx +72 -0
- package/templates/shadcn/src/lib/client.ts +8 -0
- package/templates/shadcn/src/pages/Home.tsx +15 -0
- package/templates/shadcn/src/pages/Login.tsx +70 -0
- package/templates/shadcn/src/pages/NotFound.tsx +14 -0
- package/templates/shadcn/src/pages/Register.tsx +82 -0
- package/templates/space/ROBODEV.md +7 -1
- package/templates/space/package.json +2 -2
- package/templates/shadcn/components.json +0 -21
- package/templates/shadcn/postcss.config.js +0 -6
- package/templates/shadcn/tailwind.config.ts +0 -42
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fe",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Povio UI starter frontend",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite --host",
|
|
9
|
+
"build": "bun run openapi:gen && bunx --bun vite build",
|
|
10
|
+
"clean": "rimraf dist src/openapi",
|
|
11
|
+
"lint": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --fix",
|
|
12
|
+
"lint:check": "oxlint --type-aware . ../../robodev/api ../../robodev/database.ts --quiet",
|
|
13
|
+
"format": "oxfmt -c \"../../oxfmt.config.js\" . ../../robodev/api ../../robodev/database.ts",
|
|
14
|
+
"format:check": "oxfmt -c \"../../oxfmt.config.js\" --check . ../../robodev/api ../../robodev/database.ts",
|
|
15
|
+
"ts:check": "tsc -b --noEmit",
|
|
16
|
+
"test": "vitest run --passWithNoTests",
|
|
17
|
+
"check": "bun ts:check && bun lint:check && bun format:check",
|
|
18
|
+
"openapi:gen": "openapi-codegen generate && oxfmt -c \"../../oxfmt.config.js\" ./src/openapi",
|
|
19
|
+
"openapi:check": "openapi-codegen check"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@casl/ability": "^6.8.1",
|
|
23
|
+
"@casl/react": "^5.0.1",
|
|
24
|
+
"@fontsource-variable/inter": "^5.2.8",
|
|
25
|
+
"@hookform/resolvers": "^5.2.2",
|
|
26
|
+
"@internationalized/date": "^3.11.0",
|
|
27
|
+
"@povio/openapi-codegen-cli": "3.1.0",
|
|
28
|
+
"@povio/resolve-config": "1.3.0",
|
|
29
|
+
"@povio/ui": "3.4.0-rc.12",
|
|
30
|
+
"@tanstack/react-form": "^1.33.2",
|
|
31
|
+
"@tanstack/react-query": "^5.90.21",
|
|
32
|
+
"@tanstack/react-router": "^1.136.8",
|
|
33
|
+
"@tanstack/react-table": "^8.21.3",
|
|
34
|
+
"@tanstack/router-plugin": "^1.136.8",
|
|
35
|
+
"axios": "^1.13.2",
|
|
36
|
+
"class-variance-authority": "^0.7.1",
|
|
37
|
+
"clsx": "^2.1.1",
|
|
38
|
+
"i18next": "^25.7.3",
|
|
39
|
+
"loglevel": "^1.9.2",
|
|
40
|
+
"lucide-react": "^1.28.0",
|
|
41
|
+
"luxon": "^3.7.2",
|
|
42
|
+
"react": "19.2.4",
|
|
43
|
+
"react-aria": "^3.46.0",
|
|
44
|
+
"react-aria-components": "^1.15.1",
|
|
45
|
+
"react-dom": "19.2.4",
|
|
46
|
+
"react-hook-form": "^7.65.0",
|
|
47
|
+
"react-i18next": "^16.5.1",
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
52
|
+
"@types/luxon": "^3.7.1",
|
|
53
|
+
"@types/node": "^24.10.9",
|
|
54
|
+
"@types/react": "^19.2.7",
|
|
55
|
+
"@types/react-dom": "^19.2.3",
|
|
56
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
57
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
58
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
59
|
+
"oxfmt": "^0.45.0",
|
|
60
|
+
"oxlint": "1.60.0",
|
|
61
|
+
"oxlint-tsgolint": "0.21.0",
|
|
62
|
+
"react-icons-sprite": "^1.1.1",
|
|
63
|
+
"rimraf": "^6.1.2",
|
|
64
|
+
"tailwindcss": "^4.2.4",
|
|
65
|
+
"tailwindcss-react-aria-components": "^2.1.0",
|
|
66
|
+
"tsx": "^4.22.1",
|
|
67
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
68
|
+
"vite": "^8.0.0",
|
|
69
|
+
"vitest": "^3.2.4"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="239.36" height="239.36"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="239.36" height="239.36"><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 239.36 239.36"><defs><style>.cls-1{fill:#3c60ad;}.cls-2{fill:none;}.cls-3{clip-path:url(#clip-path);}.cls-4{mask:url(#mask);}.cls-5{fill:#fff;}</style><clipPath id="clip-path"><rect class="cls-2" width="239.36" height="239.36"></rect></clipPath><mask id="mask" x="0" y="0" width="239.36" height="239.36" maskUnits="userSpaceOnUse"><g id="mask0_21778_2258" data-name="mask0 21778 2258"><path class="cls-1" d="M0,29.92A29.93,29.93,0,0,1,29.92,0H209.44a29.93,29.93,0,0,1,29.92,29.92V209.44a29.93,29.93,0,0,1-29.92,29.92H29.92A29.93,29.93,0,0,1,0,209.44Z"></path></g></mask></defs><g class="cls-3"><g class="cls-4"><path class="cls-5" d="M0,29.92A29.93,29.93,0,0,1,29.92,0H209.44a29.93,29.93,0,0,1,29.92,29.92V209.44a29.93,29.93,0,0,1-29.92,29.92H29.92A29.93,29.93,0,0,1,0,209.44Z"></path><path d="M119.8,49.55c26.68,0,48.41,22.8,48.5,50.63,0,28.18-21.73,51.06-48.5,51.06A46.64,46.64,0,0,1,83.31,133.4v56.54h-12V100.27C71.29,72.35,93,49.55,119.8,49.55Zm0,12.29c-20.15,0-36.49,17.22-36.49,38.43,0,21.46,16.34,39,36.49,39s36.39-17.5,36.48-39C156.28,79.06,139.94,61.84,119.8,61.84Z"></path></g></g></svg></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
|
2
|
+
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
|
3
|
+
</style></svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Povio",
|
|
3
|
+
"short_name": "Povio",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "/web-app-manifest-192x192.png",
|
|
7
|
+
"sizes": "192x192",
|
|
8
|
+
"type": "image/png",
|
|
9
|
+
"purpose": "maskable"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"src": "/web-app-manifest-512x512.png",
|
|
13
|
+
"sizes": "512x512",
|
|
14
|
+
"type": "image/png",
|
|
15
|
+
"purpose": "maskable"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"theme_color": "#ffffff",
|
|
19
|
+
"background_color": "#ffffff",
|
|
20
|
+
"display": "standalone"
|
|
21
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{
|
|
2
|
+
"appName": "Tiny Template",
|
|
3
|
+
"appDescription": "A template for building web applications",
|
|
4
|
+
"shared": {
|
|
5
|
+
"notFound": {
|
|
6
|
+
"head": {
|
|
7
|
+
"title": "Page not found",
|
|
8
|
+
"description": "The page you are looking for doesn't exist."
|
|
9
|
+
},
|
|
10
|
+
"heading": "Page not found",
|
|
11
|
+
"subheading": "The page you are looking for doesn't exist.",
|
|
12
|
+
"homeBtn": "Go to home"
|
|
13
|
+
},
|
|
14
|
+
"errorFallback": {
|
|
15
|
+
"heading": "Something went wrong",
|
|
16
|
+
"subheading": "Error: {{error}}",
|
|
17
|
+
"retryBtn": "Try again"
|
|
18
|
+
},
|
|
19
|
+
"apiErrors": {
|
|
20
|
+
"requestFailed": "Request failed: {{error}}",
|
|
21
|
+
"unknown": "An unknown error occurred."
|
|
22
|
+
},
|
|
23
|
+
"seedData": {
|
|
24
|
+
"actionButton": "Reset seed data",
|
|
25
|
+
"confirmation": {
|
|
26
|
+
"heading": "Reset seed data?",
|
|
27
|
+
"description": "This will reset all local data to the initial seed state.",
|
|
28
|
+
"confirm": "Reset",
|
|
29
|
+
"cancel": "Cancel"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"actions": {
|
|
33
|
+
"back": "Back"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"auth": {
|
|
37
|
+
"wrapper": {
|
|
38
|
+
"title": "Administration portal",
|
|
39
|
+
"description": "Tiny",
|
|
40
|
+
"watermark": "© {{year}} Tiny • Developed and maintained by Povio"
|
|
41
|
+
},
|
|
42
|
+
"shared": {
|
|
43
|
+
"errors": {
|
|
44
|
+
"invalidCredentials": "Invalid email or password.",
|
|
45
|
+
"userAlreadyExists": "An account with this email already exists.",
|
|
46
|
+
"identityNotFound": "We could not find this account.",
|
|
47
|
+
"notConfirmed": "This account has not been confirmed yet."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"login": {
|
|
51
|
+
"head": {
|
|
52
|
+
"title": "Login",
|
|
53
|
+
"description": "Login to your account"
|
|
54
|
+
},
|
|
55
|
+
"form": {
|
|
56
|
+
"title": "Login",
|
|
57
|
+
"email": "Email",
|
|
58
|
+
"emailPlaceholder": "Enter your email",
|
|
59
|
+
"password": "Password",
|
|
60
|
+
"passwordPlaceholder": "Enter your password",
|
|
61
|
+
"submit": "Login",
|
|
62
|
+
"submitLoading": "Logging in...",
|
|
63
|
+
"demoLogin": "Login as demo user",
|
|
64
|
+
"noAccount": "Don't have an account?",
|
|
65
|
+
"registerLink": "Register",
|
|
66
|
+
"loginFailed": "Login failed",
|
|
67
|
+
"resetSeedData": "Reset seed data"
|
|
68
|
+
},
|
|
69
|
+
"confirmations": {
|
|
70
|
+
"resetSeed": {
|
|
71
|
+
"heading": "Reset seed data?",
|
|
72
|
+
"description": "This will reset all local data to the initial seed state.",
|
|
73
|
+
"confirm": "Reset",
|
|
74
|
+
"cancel": "Cancel"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"register": {
|
|
79
|
+
"head": {
|
|
80
|
+
"title": "Register",
|
|
81
|
+
"description": "Create a new account"
|
|
82
|
+
},
|
|
83
|
+
"form": {
|
|
84
|
+
"title": "Register",
|
|
85
|
+
"description": "Create a new account",
|
|
86
|
+
"email": "Email",
|
|
87
|
+
"emailPlaceholder": "Enter your email",
|
|
88
|
+
"name": "Name",
|
|
89
|
+
"namePlaceholder": "Enter your name",
|
|
90
|
+
"password": "Password",
|
|
91
|
+
"passwordPlaceholder": "Enter your password",
|
|
92
|
+
"confirmPassword": "Confirm password",
|
|
93
|
+
"confirmPasswordPlaceholder": "Confirm your password",
|
|
94
|
+
"confirmPasswordRequired": "Confirm password is required",
|
|
95
|
+
"passwordsDoNotMatch": "Passwords do not match",
|
|
96
|
+
"passwordRequirements": "Password must be at least 12 characters long.",
|
|
97
|
+
"submit": "Register",
|
|
98
|
+
"registrationFailed": "Registration failed",
|
|
99
|
+
"hasAccount": "Already have an account?",
|
|
100
|
+
"loginLink": "Login"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"setPassword": {
|
|
104
|
+
"form": {
|
|
105
|
+
"title": "Set your password",
|
|
106
|
+
"welcome": "Welcome, {{firstName}} {{lastName}}. Please set your password to complete your account.",
|
|
107
|
+
"tokenNote": "Enter the invitation token you received by email.",
|
|
108
|
+
"token": "Invitation token",
|
|
109
|
+
"tokenPlaceholder": "Enter your invitation token",
|
|
110
|
+
"tokenChecking": "Checking token...",
|
|
111
|
+
"tokenError": "{{error}}",
|
|
112
|
+
"invalidToken": "Invalid or expired token",
|
|
113
|
+
"email": "Email",
|
|
114
|
+
"password": "Password",
|
|
115
|
+
"passwordPlaceholder": "Enter your password",
|
|
116
|
+
"confirmPassword": "Confirm password",
|
|
117
|
+
"confirmPasswordPlaceholder": "Confirm your password",
|
|
118
|
+
"passwordRequirements": "Password must be at least 8 characters long.",
|
|
119
|
+
"submit": "Set password",
|
|
120
|
+
"setPasswordFailed": "Setting password failed",
|
|
121
|
+
"hasPassword": "Already have a password?",
|
|
122
|
+
"loginLink": "Login"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"layout": {
|
|
127
|
+
"header": {
|
|
128
|
+
"logo": "Tiny",
|
|
129
|
+
"openMenu": "Open menu",
|
|
130
|
+
"closeMenu": "Close menu",
|
|
131
|
+
"nav": {
|
|
132
|
+
"home": "Home"
|
|
133
|
+
},
|
|
134
|
+
"profile": "Profile",
|
|
135
|
+
"logout": "Logout"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"home": {
|
|
139
|
+
"head": {
|
|
140
|
+
"title": "Home",
|
|
141
|
+
"description": "Welcome to the app!"
|
|
142
|
+
},
|
|
143
|
+
"title": "Home",
|
|
144
|
+
"message": "Welcome to the app!"
|
|
145
|
+
},
|
|
146
|
+
"profile": {
|
|
147
|
+
"head": {
|
|
148
|
+
"title": "Profile",
|
|
149
|
+
"description": "Your profile"
|
|
150
|
+
},
|
|
151
|
+
"form": {
|
|
152
|
+
"title": "Profile",
|
|
153
|
+
"description": "Update your profile details",
|
|
154
|
+
"email": "Email",
|
|
155
|
+
"emailPlaceholder": "Enter your email",
|
|
156
|
+
"name": "Name",
|
|
157
|
+
"namePlaceholder": "Enter your name",
|
|
158
|
+
"submit": "Save",
|
|
159
|
+
"submitLoading": "Saving...",
|
|
160
|
+
"notifications": {
|
|
161
|
+
"success": "Profile updated",
|
|
162
|
+
"error": "Failed to update profile"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RestClient } from "@povio/openapi-codegen-cli";
|
|
2
|
+
|
|
3
|
+
import { AuthorizationHeaderInterceptor } from "@/clients/rest/interceptors/authorization-header.interceptor";
|
|
4
|
+
import { RefreshTokenInterceptor } from "@/clients/rest/interceptors/refresh-token.interceptor";
|
|
5
|
+
import { ResponseInterceptor } from "@/clients/rest/interceptors/response.interceptor";
|
|
6
|
+
import { AppConfig } from "@/config/app.config";
|
|
7
|
+
import { AppErrorHandler } from "@/clients/rest/app-error-handler";
|
|
8
|
+
|
|
9
|
+
export const AppRestClient = new RestClient({
|
|
10
|
+
config: {
|
|
11
|
+
baseURL: AppConfig.api.url,
|
|
12
|
+
},
|
|
13
|
+
errorHandler: AppErrorHandler,
|
|
14
|
+
interceptors: [AuthorizationHeaderInterceptor, RefreshTokenInterceptor, ResponseInterceptor],
|
|
15
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/config/jwt.config";
|
|
2
|
+
|
|
3
|
+
type TokenListener = () => void;
|
|
4
|
+
|
|
5
|
+
const listeners = new Set<TokenListener>();
|
|
6
|
+
let storageListenerCount = 0;
|
|
7
|
+
|
|
8
|
+
const isBrowser = () => typeof window !== "undefined";
|
|
9
|
+
|
|
10
|
+
const readToken = (key: string) => {
|
|
11
|
+
if (!isBrowser()) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return window.localStorage.getItem(key);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const writeToken = (key: string, token: string | null) => {
|
|
23
|
+
if (!isBrowser()) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (token) {
|
|
29
|
+
window.localStorage.setItem(key, token);
|
|
30
|
+
} else {
|
|
31
|
+
window.localStorage.removeItem(key);
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Keep in-memory auth subscribers in sync even if storage is unavailable.
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const emitChange = () => {
|
|
39
|
+
for (const listener of listeners) {
|
|
40
|
+
listener();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const onStorageChange = (event: StorageEvent) => {
|
|
45
|
+
if (event.storageArea !== window.localStorage) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (event.key === ACCESS_TOKEN_KEY || event.key === REFRESH_TOKEN_KEY || event.key === null) {
|
|
50
|
+
emitChange();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const authTokenStore = {
|
|
55
|
+
getAccessToken: () => readToken(ACCESS_TOKEN_KEY),
|
|
56
|
+
getRefreshToken: () => readToken(REFRESH_TOKEN_KEY),
|
|
57
|
+
getServerSnapshot: () => null,
|
|
58
|
+
subscribe: (listener: TokenListener) => {
|
|
59
|
+
listeners.add(listener);
|
|
60
|
+
|
|
61
|
+
if (isBrowser()) {
|
|
62
|
+
if (storageListenerCount === 0) {
|
|
63
|
+
window.addEventListener("storage", onStorageChange);
|
|
64
|
+
}
|
|
65
|
+
storageListenerCount += 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
listeners.delete(listener);
|
|
70
|
+
|
|
71
|
+
if (isBrowser()) {
|
|
72
|
+
storageListenerCount -= 1;
|
|
73
|
+
if (storageListenerCount === 0) {
|
|
74
|
+
window.removeEventListener("storage", onStorageChange);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
setAccessToken: (token: string | null) => {
|
|
80
|
+
writeToken(ACCESS_TOKEN_KEY, token);
|
|
81
|
+
emitChange();
|
|
82
|
+
},
|
|
83
|
+
setRefreshToken: (token: string | null) => {
|
|
84
|
+
writeToken(REFRESH_TOKEN_KEY, token);
|
|
85
|
+
emitChange();
|
|
86
|
+
},
|
|
87
|
+
setTokens: (accessToken: string | null, refreshToken?: string | null) => {
|
|
88
|
+
writeToken(ACCESS_TOKEN_KEY, accessToken);
|
|
89
|
+
|
|
90
|
+
if (refreshToken !== undefined) {
|
|
91
|
+
writeToken(REFRESH_TOKEN_KEY, refreshToken);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
emitChange();
|
|
95
|
+
},
|
|
96
|
+
clear: () => {
|
|
97
|
+
writeToken(ACCESS_TOKEN_KEY, null);
|
|
98
|
+
writeToken(REFRESH_TOKEN_KEY, null);
|
|
99
|
+
emitChange();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ErrorEntry, ErrorHandler } from "@povio/openapi-codegen-cli";
|
|
2
|
+
|
|
3
|
+
export enum AuthErrorCodes {
|
|
4
|
+
InvalidCredentials = "invalid-credentials",
|
|
5
|
+
LocalAuthnIdentityExists = "identity-already-exists",
|
|
6
|
+
IdentityNotFound = "identity-not-found",
|
|
7
|
+
NotConfirmed = "identity-not-confirmed",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const authErrors: ErrorEntry<AuthErrorCodes>[] = [
|
|
11
|
+
{
|
|
12
|
+
code: AuthErrorCodes.InvalidCredentials,
|
|
13
|
+
getMessage: (t) => t("auth.shared.errors.invalidCredentials"),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
code: AuthErrorCodes.LocalAuthnIdentityExists,
|
|
17
|
+
getMessage: (t) => t("auth.shared.errors.userAlreadyExists"),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
code: AuthErrorCodes.IdentityNotFound,
|
|
21
|
+
getMessage: (t) => t("auth.shared.errors.identityNotFound"),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: AuthErrorCodes.NotConfirmed,
|
|
25
|
+
getMessage: (t) => t("auth.shared.errors.notConfirmed"),
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const entries = [...authErrors];
|
|
30
|
+
|
|
31
|
+
export const AppErrorHandler = new ErrorHandler({ entries });
|
package/templates/povio/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RestInterceptor } from "@povio/openapi-codegen-cli";
|
|
2
|
+
|
|
3
|
+
import { authTokenStore } from "@/clients/auth-token-store";
|
|
4
|
+
|
|
5
|
+
export const AuthorizationHeaderInterceptor = new RestInterceptor((client) => {
|
|
6
|
+
return client.interceptors.request.use(async (config) => {
|
|
7
|
+
const accessToken = authTokenStore.getAccessToken();
|
|
8
|
+
if (accessToken != null) {
|
|
9
|
+
// oxlint-disable-next-line no-param-reassign
|
|
10
|
+
config.headers.Authorization = `Bearer ${accessToken}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return config;
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RestInterceptor } from "@povio/openapi-codegen-cli";
|
|
2
|
+
|
|
3
|
+
import { AuthErrorCodes } from "@/clients/rest/app-error-handler";
|
|
4
|
+
import { UserAuthApi } from "@/openapi/userAuth/userAuth.api";
|
|
5
|
+
import { authTokenStore } from "@/clients/auth-token-store";
|
|
6
|
+
|
|
7
|
+
const REFRESH_PATH = "/api/user/auth/refresh";
|
|
8
|
+
|
|
9
|
+
export const RefreshTokenInterceptor = new RestInterceptor((client) => {
|
|
10
|
+
return client.interceptors.response.use(
|
|
11
|
+
(response) => response,
|
|
12
|
+
// oxlint-disable-next-line promise/prefer-await-to-callbacks
|
|
13
|
+
async (error) => {
|
|
14
|
+
const originalRequest = error.config;
|
|
15
|
+
|
|
16
|
+
if (error.response?.data?.code === AuthErrorCodes.InvalidCredentials) {
|
|
17
|
+
return Promise.reject(error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The refresh call itself must not trigger another refresh: a 401 from it would otherwise
|
|
21
|
+
// re-enter this interceptor and loop until the tab is closed.
|
|
22
|
+
const isRefreshCall = typeof originalRequest?.url === "string" && originalRequest.url.includes(REFRESH_PATH);
|
|
23
|
+
|
|
24
|
+
if (error.response?.status === 401 && !originalRequest._retry && !isRefreshCall) {
|
|
25
|
+
originalRequest._retry = true;
|
|
26
|
+
const refreshToken = authTokenStore.getRefreshToken();
|
|
27
|
+
if (!refreshToken) {
|
|
28
|
+
authTokenStore.clear();
|
|
29
|
+
return Promise.reject(error);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const { accessToken, refreshToken: newRefreshToken } = await UserAuthApi.accessToken({ refreshToken });
|
|
33
|
+
authTokenStore.setTokens(accessToken, newRefreshToken || null);
|
|
34
|
+
|
|
35
|
+
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
|
36
|
+
|
|
37
|
+
return client(originalRequest);
|
|
38
|
+
} catch {
|
|
39
|
+
authTokenStore.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Promise.reject(error);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RestInterceptor } from "@povio/openapi-codegen-cli";
|
|
2
|
+
import type { AxiosError, AxiosResponse } from "axios";
|
|
3
|
+
|
|
4
|
+
export const ResponseInterceptor = new RestInterceptor((client) => {
|
|
5
|
+
return client.interceptors.response.use(
|
|
6
|
+
async (response: AxiosResponse) => {
|
|
7
|
+
return {
|
|
8
|
+
...response,
|
|
9
|
+
data: response.data === "" ? undefined : response.data,
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
// oxlint-disable-next-line promise/prefer-await-to-callbacks
|
|
13
|
+
async (error: AxiosError) => {
|
|
14
|
+
throw error;
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useTranslation } from "react-i18next";
|
|
2
|
+
|
|
3
|
+
import { NotFound } from "@/components/shared/error/NotFound";
|
|
4
|
+
import { AppHead } from "@/components/shared/head/AppHead";
|
|
5
|
+
|
|
6
|
+
export const NotFoundPage = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<AppHead
|
|
12
|
+
title={t(($) => $.shared.notFound.head.title)}
|
|
13
|
+
description={t(($) => $.shared.notFound.head.description)}
|
|
14
|
+
/>
|
|
15
|
+
<NotFound />
|
|
16
|
+
</>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Typography } from "@povio/ui";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
import { BrandLogo } from "@/components/shared/branding/BrandLogo";
|
|
5
|
+
|
|
6
|
+
export function AuthBrandPanel() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<section className="relative hidden flex-1 overflow-hidden bg-elevation-fill-default-2 lg:flex lg:flex-col lg:justify-between">
|
|
11
|
+
<div className="relative z-raised flex flex-col gap-1 px-16 pt-20 xl:px-24 xl:pt-24">
|
|
12
|
+
<Typography
|
|
13
|
+
as="p"
|
|
14
|
+
className="text-text-default-2"
|
|
15
|
+
size="title-3"
|
|
16
|
+
>
|
|
17
|
+
{t(($) => $.auth.wrapper.title)}
|
|
18
|
+
</Typography>
|
|
19
|
+
<Typography
|
|
20
|
+
as="h1"
|
|
21
|
+
className="text-text-default-2"
|
|
22
|
+
size="title-1"
|
|
23
|
+
variant="prominent-1"
|
|
24
|
+
>
|
|
25
|
+
{t(($) => $.auth.wrapper.description)}
|
|
26
|
+
</Typography>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<BrandLogo className="absolute -right-12 top-10 text-[10rem] leading-none text-[#e1e9ee]" />
|
|
30
|
+
|
|
31
|
+
<svg
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
className="absolute bottom-0 left-0 h-auto w-full opacity-20"
|
|
34
|
+
viewBox="0 0 760 390"
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
d="M0 245 80 202l62 22 98-85 92 77 68-45 76 53 84-113 74 95 126-50v234H0Z"
|
|
38
|
+
fill="#006ab6"
|
|
39
|
+
/>
|
|
40
|
+
<path
|
|
41
|
+
d="m52 390 132-120 78 61 106-130 91 108 69-59 128 140Z"
|
|
42
|
+
fill="#006ab6"
|
|
43
|
+
opacity=".4"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
|
|
47
|
+
<div className="relative z-raised flex flex-col items-center gap-3 pb-8">
|
|
48
|
+
<BrandLogo className="text-interactive-contained-primary-idle" />
|
|
49
|
+
<Typography
|
|
50
|
+
as="span"
|
|
51
|
+
className="text-text-default-2"
|
|
52
|
+
size="body-4"
|
|
53
|
+
variant="prominent-1"
|
|
54
|
+
>
|
|
55
|
+
{t(($) => $.auth.wrapper.watermark, { year: new Date().getFullYear() })}
|
|
56
|
+
</Typography>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
import { BrandLogo } from "@/components/shared/branding/BrandLogo";
|
|
4
|
+
|
|
5
|
+
import { AuthBrandPanel } from "./AuthBrandPanel";
|
|
6
|
+
|
|
7
|
+
export function AuthLayout({ children }: PropsWithChildren) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex min-h-dvh w-full overflow-hidden bg-elevation-fill-default-2">
|
|
10
|
+
<AuthBrandPanel />
|
|
11
|
+
|
|
12
|
+
<section className="m-4 flex w-full flex-1 flex-col rounded-sm bg-elevation-fill-default-1 lg:m-0 lg:rounded-none">
|
|
13
|
+
<div className="flex flex-1 items-center justify-center px-4 py-10 sm:px-8">
|
|
14
|
+
<div className="w-full max-w-[440px]">{children}</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div className="flex justify-center pb-6 lg:hidden">
|
|
18
|
+
<BrandLogo className="text-interactive-contained-primary-idle" />
|
|
19
|
+
</div>
|
|
20
|
+
</section>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|