blacksmith-cli 0.1.1

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.
Files changed (103) hide show
  1. package/README.md +210 -0
  2. package/bin/blacksmith.js +20 -0
  3. package/dist/index.js +4404 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/templates/backend/.env.example.hbs +10 -0
  7. package/src/templates/backend/apps/__init__.py.hbs +0 -0
  8. package/src/templates/backend/apps/users/__init__.py.hbs +0 -0
  9. package/src/templates/backend/apps/users/admin.py.hbs +26 -0
  10. package/src/templates/backend/apps/users/managers.py.hbs +25 -0
  11. package/src/templates/backend/apps/users/models.py.hbs +25 -0
  12. package/src/templates/backend/apps/users/serializers.py.hbs +94 -0
  13. package/src/templates/backend/apps/users/tests.py.hbs +47 -0
  14. package/src/templates/backend/apps/users/urls.py.hbs +10 -0
  15. package/src/templates/backend/apps/users/views.py.hbs +175 -0
  16. package/src/templates/backend/config/__init__.py.hbs +0 -0
  17. package/src/templates/backend/config/asgi.py.hbs +9 -0
  18. package/src/templates/backend/config/settings/__init__.py.hbs +13 -0
  19. package/src/templates/backend/config/settings/base.py.hbs +117 -0
  20. package/src/templates/backend/config/settings/development.py.hbs +19 -0
  21. package/src/templates/backend/config/settings/production.py.hbs +31 -0
  22. package/src/templates/backend/config/urls.py.hbs +26 -0
  23. package/src/templates/backend/config/wsgi.py.hbs +9 -0
  24. package/src/templates/backend/manage.py.hbs +22 -0
  25. package/src/templates/backend/requirements.txt.hbs +7 -0
  26. package/src/templates/frontend/.env.hbs +1 -0
  27. package/src/templates/frontend/index.html.hbs +13 -0
  28. package/src/templates/frontend/openapi-ts.config.ts.hbs +29 -0
  29. package/src/templates/frontend/package.json.hbs +44 -0
  30. package/src/templates/frontend/postcss.config.js.hbs +6 -0
  31. package/src/templates/frontend/src/api/client.ts.hbs +110 -0
  32. package/src/templates/frontend/src/api/generated/.gitkeep +0 -0
  33. package/src/templates/frontend/src/api/generated/client.gen.ts +13 -0
  34. package/src/templates/frontend/src/api/query-client.ts.hbs +22 -0
  35. package/src/templates/frontend/src/app.tsx.hbs +30 -0
  36. package/src/templates/frontend/src/features/auth/adapter.ts.hbs +198 -0
  37. package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +32 -0
  38. package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +27 -0
  39. package/src/templates/frontend/src/features/auth/index.ts.hbs +3 -0
  40. package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +37 -0
  41. package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +36 -0
  42. package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +36 -0
  43. package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +41 -0
  44. package/src/templates/frontend/src/features/auth/routes.tsx.hbs +13 -0
  45. package/src/templates/frontend/src/main.tsx.hbs +10 -0
  46. package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +36 -0
  47. package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +69 -0
  48. package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +14 -0
  49. package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +21 -0
  50. package/src/templates/frontend/src/pages/dashboard/index.ts.hbs +1 -0
  51. package/src/templates/frontend/src/pages/dashboard/routes.tsx.hbs +7 -0
  52. package/src/templates/frontend/src/pages/home/components/features-grid.tsx.hbs +88 -0
  53. package/src/templates/frontend/src/pages/home/components/getting-started.tsx.hbs +88 -0
  54. package/src/templates/frontend/src/pages/home/components/hero-section.tsx.hbs +47 -0
  55. package/src/templates/frontend/src/pages/home/components/resources-section.tsx.hbs +34 -0
  56. package/src/templates/frontend/src/pages/home/home.tsx.hbs +20 -0
  57. package/src/templates/frontend/src/pages/home/index.ts.hbs +1 -0
  58. package/src/templates/frontend/src/pages/home/routes.tsx.hbs +7 -0
  59. package/src/templates/frontend/src/router/auth-guard.tsx.hbs +57 -0
  60. package/src/templates/frontend/src/router/error-boundary.tsx.hbs +61 -0
  61. package/src/templates/frontend/src/router/index.tsx.hbs +12 -0
  62. package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +68 -0
  63. package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +137 -0
  64. package/src/templates/frontend/src/router/paths.ts.hbs +38 -0
  65. package/src/templates/frontend/src/router/routes.tsx.hbs +64 -0
  66. package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +20 -0
  67. package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +31 -0
  68. package/src/templates/frontend/src/shared/hooks/api-error.ts.hbs +147 -0
  69. package/src/templates/frontend/src/shared/hooks/use-api-mutation.ts.hbs +88 -0
  70. package/src/templates/frontend/src/shared/hooks/use-api-query.ts.hbs +66 -0
  71. package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +10 -0
  72. package/src/templates/frontend/src/styles/globals.css.hbs +62 -0
  73. package/src/templates/frontend/src/vite-env.d.ts.hbs +1 -0
  74. package/src/templates/frontend/tailwind.config.js.hbs +73 -0
  75. package/src/templates/frontend/tsconfig.app.json.hbs +25 -0
  76. package/src/templates/frontend/tsconfig.json.hbs +7 -0
  77. package/src/templates/frontend/tsconfig.node.json.hbs +18 -0
  78. package/src/templates/frontend/vite.config.ts.hbs +21 -0
  79. package/src/templates/resource/backend/__init__.py.hbs +0 -0
  80. package/src/templates/resource/backend/admin.py.hbs +10 -0
  81. package/src/templates/resource/backend/models.py.hbs +24 -0
  82. package/src/templates/resource/backend/serializers.py.hbs +21 -0
  83. package/src/templates/resource/backend/tests.py.hbs +35 -0
  84. package/src/templates/resource/backend/urls.py.hbs +10 -0
  85. package/src/templates/resource/backend/views.py.hbs +32 -0
  86. package/src/templates/resource/frontend/components/{{kebab}}-card.tsx.hbs +39 -0
  87. package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +106 -0
  88. package/src/templates/resource/frontend/components/{{kebab}}-list.tsx.hbs +49 -0
  89. package/src/templates/resource/frontend/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
  90. package/src/templates/resource/frontend/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
  91. package/src/templates/resource/frontend/index.ts.hbs +6 -0
  92. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +33 -0
  93. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
  94. package/src/templates/resource/frontend/routes.tsx.hbs +15 -0
  95. package/src/templates/resource/pages/components/{{kebab}}-card.tsx.hbs +39 -0
  96. package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +106 -0
  97. package/src/templates/resource/pages/components/{{kebab}}-list.tsx.hbs +49 -0
  98. package/src/templates/resource/pages/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
  99. package/src/templates/resource/pages/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
  100. package/src/templates/resource/pages/index.ts.hbs +6 -0
  101. package/src/templates/resource/pages/routes.tsx.hbs +15 -0
  102. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +33 -0
  103. package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
@@ -0,0 +1,26 @@
1
+ """
2
+ URL configuration for {{projectName}}.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.urls import path, include
7
+ from drf_spectacular.views import (
8
+ SpectacularAPIView,
9
+ SpectacularSwaggerView,
10
+ SpectacularRedocView,
11
+ )
12
+
13
+ urlpatterns = [
14
+ path('admin/', admin.site.urls),
15
+
16
+ # OpenAPI schema & documentation
17
+ path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
18
+ path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
19
+ path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
20
+
21
+ # Auth endpoints
22
+ path('api/auth/', include('apps.users.urls')),
23
+
24
+ # Resource endpoints (auto-registered by blacksmith make:resource)
25
+ # blacksmith:urls
26
+ ]
@@ -0,0 +1,9 @@
1
+ """
2
+ WSGI config for {{projectName}}.
3
+ """
4
+
5
+ import os
6
+ from django.core.wsgi import get_wsgi_application
7
+
8
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
9
+ application = get_wsgi_application()
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python
2
+ """Django's command-line utility for administrative tasks."""
3
+ import os
4
+ import sys
5
+
6
+
7
+ def main():
8
+ """Run administrative tasks."""
9
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
10
+ try:
11
+ from django.core.management import execute_from_command_line
12
+ except ImportError as exc:
13
+ raise ImportError(
14
+ "Couldn't import Django. Are you sure it's installed and "
15
+ "available on your PYTHONPATH environment variable? Did you "
16
+ "forget to activate a virtual environment?"
17
+ ) from exc
18
+ execute_from_command_line(sys.argv)
19
+
20
+
21
+ if __name__ == '__main__':
22
+ main()
@@ -0,0 +1,7 @@
1
+ django>=4.2,<6.0
2
+ djangorestframework>=3.15,<4.0
3
+ drf-spectacular>=0.28,<1.0
4
+ drf-spectacular-sidecar>=2024
5
+ djangorestframework-simplejwt>=5.4,<6.0
6
+ django-cors-headers>=4.6,<5.0
7
+ python-dotenv>=1.0,<2.0
@@ -0,0 +1 @@
1
+ VITE_API_URL=http://localhost:{{backendPort}}
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{projectName}}</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,29 @@
1
+ import { defineConfig } from '@hey-api/openapi-ts'
2
+
3
+ export default defineConfig({
4
+ input: {
5
+ path: 'http://localhost:{{backendPort}}/api/schema/',
6
+ },
7
+ output: {
8
+ path: './src/api/generated',
9
+ },
10
+ plugins: [
11
+ {
12
+ name: '@hey-api/typescript',
13
+ enums: 'javascript',
14
+ },
15
+ {
16
+ name: '@hey-api/client-fetch',
17
+ },
18
+ {
19
+ name: '@hey-api/sdk',
20
+ },
21
+ {
22
+ name: '@tanstack/react-query',
23
+ queryOptions: true,
24
+ infiniteQueryOptions: true,
25
+ mutationOptions: true,
26
+ },
27
+ 'zod',
28
+ ],
29
+ })
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "{{projectName}}-frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "openapi-ts": "openapi-ts"
12
+ },
13
+ "dependencies": {
14
+ "@blacksmith-ui/auth": "^0.1.0",
15
+ "@blacksmith-ui/forms": "^0.1.0",
16
+ "@blacksmith-ui/hooks": "^0.1.0",
17
+ "@blacksmith-ui/react": "^0.1.2",
18
+ "@hey-api/client-fetch": "^0.9.0",
19
+ "@hookform/resolvers": "^5.0.0",
20
+ "@tanstack/react-query": "^5.90.0",
21
+ "@tanstack/react-query-devtools": "^5.90.0",
22
+ "clsx": "^2.1.1",
23
+ "lucide-react": "^0.400.0",
24
+ "react": "^19.1.0",
25
+ "react-dom": "^19.1.0",
26
+ "react-error-boundary": "^5.0.0",
27
+ "react-hook-form": "^7.55.0",
28
+ "react-router-dom": "^7.6.0",
29
+ "tailwind-merge": "^3.0.0",
30
+ "tailwindcss": "^3.4.0",
31
+ "zod": "^3.24.0"
32
+ },
33
+ "devDependencies": {
34
+ "@hey-api/openapi-ts": "^0.93.0",
35
+ "@types/react": "^19.1.0",
36
+ "@types/react-dom": "^19.1.0",
37
+ "@vitejs/plugin-react": "^5.0.0",
38
+ "autoprefixer": "^10.4.0",
39
+ "postcss": "^8.4.0",
40
+ "tailwindcss-animate": "^1.0.7",
41
+ "typescript": "~5.8.0",
42
+ "vite": "^6.3.0"
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * API Client Configuration
3
+ *
4
+ * Configures the @hey-api/client-fetch client with:
5
+ * - Base URL from environment
6
+ * - JWT auth interceptor
7
+ * - CSRF token interceptor (for Django)
8
+ * - 401 token refresh interceptor
9
+ *
10
+ * Generated by Blacksmith. You own this file — customize as needed.
11
+ */
12
+
13
+ import { createClient } from '@hey-api/client-fetch'
14
+
15
+ // Create the client directly (no dependency on generated files)
16
+ export const client = createClient({
17
+ baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:{{backendPort}}',
18
+ })
19
+
20
+ // In-memory token storage (more secure than localStorage)
21
+ let accessToken: string | null = null
22
+ let refreshToken: string | null = null
23
+
24
+ export function setTokens(access: string | null, refresh?: string | null) {
25
+ accessToken = access
26
+ if (refresh !== undefined) {
27
+ refreshToken = refresh
28
+ }
29
+ }
30
+
31
+ export function getAccessToken(): string | null {
32
+ return accessToken
33
+ }
34
+
35
+ export function getRefreshToken(): string | null {
36
+ return refreshToken
37
+ }
38
+
39
+ export function clearTokens() {
40
+ accessToken = null
41
+ refreshToken = null
42
+ }
43
+
44
+ // Auth interceptor — attach JWT to every request
45
+ client.interceptors.request.use((request) => {
46
+ if (accessToken) {
47
+ request.headers.set('Authorization', `Bearer ${accessToken}`)
48
+ }
49
+ return request
50
+ })
51
+
52
+ // CSRF interceptor — Django requires this for non-GET requests
53
+ client.interceptors.request.use((request) => {
54
+ const csrfToken = document.cookie
55
+ .split('; ')
56
+ .find((row) => row.startsWith('csrftoken='))
57
+ ?.split('=')[1]
58
+
59
+ if (csrfToken) {
60
+ request.headers.set('X-CSRFToken', csrfToken)
61
+ }
62
+ return request
63
+ })
64
+
65
+ // 401 interceptor — attempt token refresh, then retry
66
+ let isRefreshing = false
67
+ let refreshPromise: Promise<boolean> | null = null
68
+
69
+ async function attemptTokenRefresh(): Promise<boolean> {
70
+ if (!refreshToken) return false
71
+
72
+ try {
73
+ const response = await fetch(
74
+ `${import.meta.env.VITE_API_URL || 'http://localhost:{{backendPort}}'}/api/auth/refresh/`,
75
+ {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({ refresh: refreshToken }),
79
+ }
80
+ )
81
+
82
+ if (!response.ok) return false
83
+
84
+ const data = await response.json()
85
+ setTokens(data.access, data.refresh)
86
+ return true
87
+ } catch {
88
+ return false
89
+ }
90
+ }
91
+
92
+ client.interceptors.response.use(async (response) => {
93
+ if (response.status === 401 && !response.url.includes('/api/auth/refresh/')) {
94
+ if (!isRefreshing) {
95
+ isRefreshing = true
96
+ refreshPromise = attemptTokenRefresh().finally(() => {
97
+ isRefreshing = false
98
+ refreshPromise = null
99
+ })
100
+ }
101
+
102
+ const refreshed = await refreshPromise
103
+ if (!refreshed) {
104
+ clearTokens()
105
+ window.dispatchEvent(new CustomEvent('auth:logout'))
106
+ }
107
+ }
108
+ return response
109
+ })
110
+
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Auto-generated API Client
3
+ *
4
+ * This is a stub file that allows the app to boot before
5
+ * the first OpenAPI sync. Run `blacksmith sync` or `blacksmith dev`
6
+ * to generate the real client from your Django API schema.
7
+ *
8
+ * Generated by Blacksmith. This file will be overwritten by openapi-ts.
9
+ */
10
+
11
+ import { createClient } from '@hey-api/client-fetch'
12
+
13
+ export const client = createClient()
@@ -0,0 +1,22 @@
1
+ /**
2
+ * React Query Client Configuration
3
+ *
4
+ * Centralized QueryClient with sensible defaults.
5
+ * Generated by Blacksmith. You own this file — customize as needed.
6
+ */
7
+
8
+ import { QueryClient } from '@tanstack/react-query'
9
+
10
+ export const queryClient = new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ staleTime: 5 * 60 * 1000, // 5 minutes before data is considered stale
14
+ gcTime: 30 * 60 * 1000, // 30 minutes in garbage collection cache
15
+ retry: 2, // Retry failed queries twice
16
+ refetchOnWindowFocus: true,
17
+ },
18
+ mutations: {
19
+ retry: 0, // Never retry mutations
20
+ },
21
+ },
22
+ })
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Root Application Component
3
+ *
4
+ * Composes all providers and the router.
5
+ * Generated by Blacksmith. You own this file — customize as needed.
6
+ */
7
+
8
+ import { QueryClientProvider } from '@tanstack/react-query'
9
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
10
+ import { RouterProvider } from 'react-router-dom'
11
+ import { ThemeProvider } from '@blacksmith-ui/react'
12
+ import { queryClient } from '@/api/query-client'
13
+ import { AuthProvider } from '@/features/auth/components/auth-provider'
14
+ import { router } from '@/router'
15
+
16
+ // Initialize API client (registers interceptors)
17
+ import '@/api/client'
18
+
19
+ export function App() {
20
+ return (
21
+ <ThemeProvider defaultMode="system" storageKey="{{projectName}}-theme">
22
+ <QueryClientProvider client={queryClient}>
23
+ <AuthProvider>
24
+ <RouterProvider router={router} />
25
+ </AuthProvider>
26
+ {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
27
+ </QueryClientProvider>
28
+ </ThemeProvider>
29
+ )
30
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Blacksmith Auth Adapter
3
+ *
4
+ * Connects @blacksmith-ui/auth to the Django JWT backend.
5
+ * Implements the AuthAdapter interface using the generated API client.
6
+ *
7
+ * Generated by Blacksmith. You own this file — customize as needed.
8
+ */
9
+
10
+ import type { AuthAdapter, AuthUser, AuthResult, AuthError, SocialProvider } from '@blacksmith-ui/auth'
11
+ import { setTokens, getAccessToken, clearTokens } from '@/api/client'
12
+ import { parseApiError } from '@/shared/hooks/api-error'
13
+
14
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:{{backendPort}}'
15
+
16
+ // Internal state
17
+ let currentUser: AuthUser | null = null
18
+ let authListeners: Array<(user: AuthUser | null) => void> = []
19
+
20
+ function notifyListeners(user: AuthUser | null) {
21
+ currentUser = user
22
+ authListeners.forEach((cb) => cb(user))
23
+ }
24
+
25
+ function mapDjangoUser(data: any): AuthUser {
26
+ return {
27
+ id: String(data.id),
28
+ email: data.email,
29
+ displayName: data.first_name
30
+ ? `${data.first_name}${data.last_name ? ' ' + data.last_name : ''}`
31
+ : null,
32
+ photoURL: null,
33
+ emailVerified: true,
34
+ providerId: 'password',
35
+ }
36
+ }
37
+
38
+ function toAuthError(error: unknown): AuthError {
39
+ const parsed = parseApiError(error)
40
+
41
+ // If there are field-level errors, include them in the message
42
+ const fieldMessages = Object.entries(parsed.fieldErrors)
43
+ .map(([field, msgs]) => `${field}: ${msgs.join(', ')}`)
44
+ .join('. ')
45
+
46
+ const message = fieldMessages
47
+ ? `${parsed.message} ${fieldMessages}`
48
+ : parsed.message
49
+
50
+ return { code: `auth/${parsed.status || 'error'}`, message }
51
+ }
52
+
53
+ async function apiFetch(path: string, options: RequestInit = {}): Promise<any> {
54
+ const response = await fetch(`${API_URL}${path}`, {
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ ...(getAccessToken() ? { Authorization: `Bearer ${getAccessToken()}` } : {}),
58
+ },
59
+ ...options,
60
+ })
61
+
62
+ const data = await response.json().catch(() => null)
63
+
64
+ if (!response.ok) {
65
+ throw { status: response.status, error: data || { detail: response.statusText } }
66
+ }
67
+
68
+ return data
69
+ }
70
+
71
+ async function fetchCurrentUser(): Promise<AuthUser> {
72
+ const data = await apiFetch('/api/auth/me/')
73
+ return mapDjangoUser(data)
74
+ }
75
+
76
+ export function createBlacksmithAuthAdapter(): AuthAdapter {
77
+ // Try to restore session on init
78
+ ;(async () => {
79
+ try {
80
+ if (getAccessToken()) {
81
+ const user = await fetchCurrentUser()
82
+ notifyListeners(user)
83
+ }
84
+ } catch {
85
+ clearTokens()
86
+ notifyListeners(null)
87
+ }
88
+ })()
89
+
90
+ return {
91
+ async signInWithEmail(email: string, password: string): Promise<AuthResult> {
92
+ try {
93
+ const data = await apiFetch('/api/auth/login/', {
94
+ method: 'POST',
95
+ body: JSON.stringify({ email, password }),
96
+ })
97
+
98
+ setTokens(data.access, data.refresh)
99
+ const user = await fetchCurrentUser()
100
+ notifyListeners(user)
101
+
102
+ return { success: true, user }
103
+ } catch (error: unknown) {
104
+ return { success: false, error: toAuthError(error) }
105
+ }
106
+ },
107
+
108
+ async signUpWithEmail(
109
+ email: string,
110
+ password: string,
111
+ displayName?: string
112
+ ): Promise<AuthResult> {
113
+ try {
114
+ const data = await apiFetch('/api/auth/register/', {
115
+ method: 'POST',
116
+ body: JSON.stringify({
117
+ email,
118
+ password,
119
+ password_confirm: password,
120
+ first_name: displayName || '',
121
+ }),
122
+ })
123
+
124
+ setTokens(data.access, data.refresh)
125
+ const user = await fetchCurrentUser()
126
+ notifyListeners(user)
127
+
128
+ return { success: true, user }
129
+ } catch (error: unknown) {
130
+ return { success: false, error: toAuthError(error) }
131
+ }
132
+ },
133
+
134
+ async signInWithSocial(_provider: SocialProvider): Promise<AuthResult> {
135
+ return {
136
+ success: false,
137
+ error: {
138
+ code: 'auth/unsupported',
139
+ message: 'Social login is not yet configured. Implement OAuth on the Django backend.',
140
+ },
141
+ }
142
+ },
143
+
144
+ async sendPasswordResetEmail(email: string) {
145
+ try {
146
+ await apiFetch('/api/auth/forgot-password/', {
147
+ method: 'POST',
148
+ body: JSON.stringify({ email }),
149
+ })
150
+ return { success: true as const }
151
+ } catch (error: unknown) {
152
+ return { success: false as const, error: toAuthError(error) }
153
+ }
154
+ },
155
+
156
+ async confirmPasswordReset(code: string, newPassword: string) {
157
+ try {
158
+ await apiFetch('/api/auth/reset-password/', {
159
+ method: 'POST',
160
+ body: JSON.stringify({
161
+ token: code,
162
+ password: newPassword,
163
+ password_confirm: newPassword,
164
+ }),
165
+ })
166
+ return { success: true as const }
167
+ } catch (error: unknown) {
168
+ return { success: false as const, error: toAuthError(error) }
169
+ }
170
+ },
171
+
172
+ async signOut(): Promise<void> {
173
+ try {
174
+ await apiFetch('/api/auth/logout/', {
175
+ method: 'POST',
176
+ })
177
+ } catch {
178
+ // Ignore errors on logout
179
+ }
180
+ clearTokens()
181
+ notifyListeners(null)
182
+ },
183
+
184
+ getCurrentUser(): AuthUser | null {
185
+ return currentUser
186
+ },
187
+
188
+ onAuthStateChanged(callback: (user: AuthUser | null) => void): () => void {
189
+ authListeners.push(callback)
190
+ // Fire immediately with current state
191
+ callback(currentUser)
192
+
193
+ return () => {
194
+ authListeners = authListeners.filter((cb) => cb !== callback)
195
+ }
196
+ },
197
+ }
198
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Auth Provider
3
+ *
4
+ * Wraps the app with @blacksmith-ui/auth's AuthProvider configured
5
+ * with the Django JWT adapter.
6
+ *
7
+ * Generated by Blacksmith. You own this file — customize as needed.
8
+ */
9
+
10
+ import { AuthProvider as BlacksmithAuthProvider } from '@blacksmith-ui/auth'
11
+ import { createBlacksmithAuthAdapter } from '../adapter'
12
+ import type { ReactNode } from 'react'
13
+
14
+ const adapter = createBlacksmithAuthAdapter()
15
+
16
+ interface Props {
17
+ children: ReactNode
18
+ }
19
+
20
+ export function AuthProvider({ children }: Props) {
21
+ return (
22
+ <BlacksmithAuthProvider
23
+ config=\{{
24
+ adapter,
25
+ // Enable social providers by adding them here:
26
+ // socialProviders: ['google', 'github'],
27
+ }}
28
+ >
29
+ {children}
30
+ </BlacksmithAuthProvider>
31
+ )
32
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Auth Hook
3
+ *
4
+ * Re-exports useAuth from @blacksmith-ui/auth for convenience.
5
+ * Import from here so your app has a single auth import path.
6
+ *
7
+ * Generated by Blacksmith. You own this file — customize as needed.
8
+ */
9
+
10
+ import { useAuth as useBlacksmithAuth } from '@blacksmith-ui/auth'
11
+
12
+ export function useAuth() {
13
+ const auth = useBlacksmithAuth()
14
+
15
+ return {
16
+ user: auth.user,
17
+ isLoading: auth.loading,
18
+ isAuthenticated: !!auth.user,
19
+ error: auth.error,
20
+ login: auth.signInWithEmail,
21
+ register: auth.signUpWithEmail,
22
+ logout: auth.signOut,
23
+ sendPasswordResetEmail: auth.sendPasswordResetEmail,
24
+ confirmPasswordReset: auth.confirmPasswordReset,
25
+ socialProviders: auth.socialProviders,
26
+ }
27
+ }
@@ -0,0 +1,3 @@
1
+ export { authRoutes } from './routes'
2
+ export { useAuth } from './hooks/use-auth'
3
+ export { AuthProvider } from './components/auth-provider'
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Forgot Password Page
3
+ *
4
+ * Uses @blacksmith-ui/auth ForgotPasswordForm connected to Django backend.
5
+ * Generated by Blacksmith. You own this file — customize as needed.
6
+ */
7
+
8
+ import { ForgotPasswordForm } from '@blacksmith-ui/auth'
9
+ import { useNavigate } from 'react-router-dom'
10
+ import { useAuth } from '../hooks/use-auth'
11
+ import { useState } from 'react'
12
+ import { Path } from '@/router/paths'
13
+
14
+ export default function ForgotPasswordPage() {
15
+ const navigate = useNavigate()
16
+ const { sendPasswordResetEmail, error } = useAuth()
17
+ const [loading, setLoading] = useState(false)
18
+
19
+ const handleSubmit = async (data: { email: string }) => {
20
+ setLoading(true)
21
+ try {
22
+ await sendPasswordResetEmail(data.email)
23
+ // Show success regardless (prevents email enumeration)
24
+ } finally {
25
+ setLoading(false)
26
+ }
27
+ }
28
+
29
+ return (
30
+ <ForgotPasswordForm
31
+ onSubmit={handleSubmit}
32
+ onLoginClick={() => navigate(Path.Login)}
33
+ error={error}
34
+ loading={loading}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Login Page
3
+ *
4
+ * Uses @blacksmith-ui/auth LoginForm connected to Django JWT backend.
5
+ * Generated by Blacksmith. You own this file — customize as needed.
6
+ */
7
+
8
+ import { LoginForm } from '@blacksmith-ui/auth'
9
+ import { useNavigate, useSearchParams } from 'react-router-dom'
10
+ import { useAuth } from '../hooks/use-auth'
11
+ import { Path } from '@/router/paths'
12
+
13
+ export default function LoginPage() {
14
+ const navigate = useNavigate()
15
+ const [searchParams] = useSearchParams()
16
+ const { login, error, isLoading } = useAuth()
17
+
18
+ const redirectTo = searchParams.get('redirect') || Path.Home
19
+
20
+ const handleSubmit = async (data: { email: string; password: string }) => {
21
+ const result = await login(data.email, data.password)
22
+ if (result.success) {
23
+ navigate(redirectTo, { replace: true })
24
+ }
25
+ }
26
+
27
+ return (
28
+ <LoginForm
29
+ onSubmit={handleSubmit}
30
+ onRegisterClick={() => navigate(Path.Register)}
31
+ onForgotPasswordClick={() => navigate(Path.ForgotPassword)}
32
+ error={error}
33
+ loading={isLoading}
34
+ />
35
+ )
36
+ }