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
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "blacksmith-cli",
3
+ "version": "0.1.1",
4
+ "description": "Fullstack Django + React framework — one command, one codebase, one mental model",
5
+ "type": "module",
6
+ "bin": {
7
+ "blacksmith": "./bin/blacksmith.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.5.0"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "start": "node dist/index.js",
17
+ "prepare": "npm run build"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "dist",
22
+ "src/templates"
23
+ ],
24
+ "keywords": [
25
+ "django",
26
+ "react",
27
+ "fullstack",
28
+ "openapi",
29
+ "scaffold",
30
+ "cli"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "chalk": "^5.4.1",
36
+ "change-case": "^5.4.4",
37
+ "chokidar": "^5.0.0",
38
+ "commander": "^13.1.0",
39
+ "concurrently": "^9.1.2",
40
+ "execa": "^9.5.2",
41
+ "handlebars": "^4.7.8",
42
+ "ora": "^8.2.0",
43
+ "pluralize": "^8.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.15.0",
47
+ "@types/pluralize": "^0.0.33",
48
+ "tsup": "^8.4.0",
49
+ "typescript": "~5.9.3"
50
+ }
51
+ }
@@ -0,0 +1,10 @@
1
+ DJANGO_ENV=development
2
+ DJANGO_SECRET_KEY=change-me-in-production
3
+ DJANGO_DEBUG=True
4
+ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
5
+
6
+ # Database (production)
7
+ # DATABASE_URL=postgres://user:password@localhost:5432/{{projectName}}
8
+
9
+ # CORS
10
+ CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
File without changes
@@ -0,0 +1,26 @@
1
+ from django.contrib import admin
2
+ from django.contrib.auth.admin import UserAdmin
3
+ from .models import CustomUser
4
+
5
+
6
+ @admin.register(CustomUser)
7
+ class CustomUserAdmin(UserAdmin):
8
+ model = CustomUser
9
+ list_display = ('email', 'first_name', 'last_name', 'is_staff', 'date_joined')
10
+ list_filter = ('is_staff', 'is_superuser', 'is_active')
11
+ search_fields = ('email', 'first_name', 'last_name')
12
+ ordering = ('-date_joined',)
13
+
14
+ fieldsets = (
15
+ (None, {'fields': ('email', 'password')}),
16
+ ('Personal info', {'fields': ('first_name', 'last_name')}),
17
+ ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
18
+ ('Important dates', {'fields': ('last_login', 'date_joined')}),
19
+ )
20
+
21
+ add_fieldsets = (
22
+ (None, {
23
+ 'classes': ('wide',),
24
+ 'fields': ('email', 'first_name', 'password1', 'password2'),
25
+ }),
26
+ )
@@ -0,0 +1,25 @@
1
+ from django.contrib.auth.models import BaseUserManager
2
+
3
+
4
+ class CustomUserManager(BaseUserManager):
5
+ """Custom user manager where email is the unique identifier."""
6
+
7
+ def create_user(self, email, password=None, **extra_fields):
8
+ if not email:
9
+ raise ValueError('The Email field must be set')
10
+ email = self.normalize_email(email)
11
+ user = self.model(email=email, **extra_fields)
12
+ user.set_password(password)
13
+ user.save(using=self._db)
14
+ return user
15
+
16
+ def create_superuser(self, email, password=None, **extra_fields):
17
+ extra_fields.setdefault('is_staff', True)
18
+ extra_fields.setdefault('is_superuser', True)
19
+
20
+ if extra_fields.get('is_staff') is not True:
21
+ raise ValueError('Superuser must have is_staff=True.')
22
+ if extra_fields.get('is_superuser') is not True:
23
+ raise ValueError('Superuser must have is_superuser=True.')
24
+
25
+ return self.create_user(email, password, **extra_fields)
@@ -0,0 +1,25 @@
1
+ from django.contrib.auth.models import AbstractUser
2
+ from django.db import models
3
+ from .managers import CustomUserManager
4
+
5
+
6
+ class CustomUser(AbstractUser):
7
+ """Custom user model with email as the primary identifier."""
8
+
9
+ username = None
10
+ email = models.EmailField('email address', unique=True)
11
+ first_name = models.CharField('first name', max_length=150)
12
+ last_name = models.CharField('last name', max_length=150, blank=True)
13
+
14
+ USERNAME_FIELD = 'email'
15
+ REQUIRED_FIELDS = ['first_name']
16
+
17
+ objects = CustomUserManager()
18
+
19
+ class Meta:
20
+ verbose_name = 'user'
21
+ verbose_name_plural = 'users'
22
+ ordering = ['-date_joined']
23
+
24
+ def __str__(self):
25
+ return self.email
@@ -0,0 +1,94 @@
1
+ from rest_framework import serializers
2
+ from django.contrib.auth import authenticate
3
+ from django.contrib.auth.password_validation import validate_password
4
+ from .models import CustomUser
5
+
6
+
7
+ class UserSerializer(serializers.ModelSerializer):
8
+ """Serializer for user profile data."""
9
+
10
+ class Meta:
11
+ model = CustomUser
12
+ fields = ['id', 'email', 'first_name', 'last_name', 'date_joined']
13
+ read_only_fields = ['id', 'email', 'date_joined']
14
+
15
+
16
+ class LoginSerializer(serializers.Serializer):
17
+ """Serializer for login requests."""
18
+
19
+ email = serializers.EmailField()
20
+ password = serializers.CharField(write_only=True)
21
+
22
+ def validate(self, attrs):
23
+ user = authenticate(
24
+ request=self.context.get('request'),
25
+ email=attrs['email'],
26
+ password=attrs['password'],
27
+ )
28
+ if not user:
29
+ raise serializers.ValidationError('Invalid email or password.')
30
+ if not user.is_active:
31
+ raise serializers.ValidationError('User account is disabled.')
32
+ attrs['user'] = user
33
+ return attrs
34
+
35
+
36
+ class RegisterSerializer(serializers.ModelSerializer):
37
+ """Serializer for user registration."""
38
+
39
+ password = serializers.CharField(
40
+ write_only=True,
41
+ validators=[validate_password],
42
+ )
43
+ password_confirm = serializers.CharField(write_only=True)
44
+
45
+ class Meta:
46
+ model = CustomUser
47
+ fields = ['email', 'first_name', 'last_name', 'password', 'password_confirm']
48
+
49
+ def validate(self, attrs):
50
+ if attrs['password'] != attrs.pop('password_confirm'):
51
+ raise serializers.ValidationError({'password_confirm': 'Passwords do not match.'})
52
+ return attrs
53
+
54
+ def create(self, validated_data):
55
+ return CustomUser.objects.create_user(**validated_data)
56
+
57
+
58
+ class TokenSerializer(serializers.Serializer):
59
+ """Serializer for JWT token responses."""
60
+
61
+ access = serializers.CharField()
62
+ refresh = serializers.CharField()
63
+
64
+
65
+ class ChangePasswordSerializer(serializers.Serializer):
66
+ """Serializer for changing password."""
67
+
68
+ old_password = serializers.CharField(write_only=True)
69
+ new_password = serializers.CharField(write_only=True, validators=[validate_password])
70
+
71
+ def validate_old_password(self, value):
72
+ user = self.context['request'].user
73
+ if not user.check_password(value):
74
+ raise serializers.ValidationError('Current password is incorrect.')
75
+ return value
76
+
77
+
78
+ class ForgotPasswordSerializer(serializers.Serializer):
79
+ """Serializer for forgot password request."""
80
+
81
+ email = serializers.EmailField()
82
+
83
+
84
+ class ResetPasswordSerializer(serializers.Serializer):
85
+ """Serializer for resetting password with token."""
86
+
87
+ token = serializers.CharField()
88
+ password = serializers.CharField(write_only=True, validators=[validate_password])
89
+ password_confirm = serializers.CharField(write_only=True)
90
+
91
+ def validate(self, attrs):
92
+ if attrs['password'] != attrs.pop('password_confirm'):
93
+ raise serializers.ValidationError({'password_confirm': 'Passwords do not match.'})
94
+ return attrs
@@ -0,0 +1,47 @@
1
+ from django.test import TestCase
2
+ from django.contrib.auth import get_user_model
3
+ from rest_framework.test import APIClient
4
+ from rest_framework import status
5
+
6
+ User = get_user_model()
7
+
8
+
9
+ class AuthTests(TestCase):
10
+ def setUp(self):
11
+ self.client = APIClient()
12
+ self.user_data = {
13
+ 'email': 'test@example.com',
14
+ 'first_name': 'Test',
15
+ 'password': 'TestPass123!',
16
+ 'password_confirm': 'TestPass123!',
17
+ }
18
+
19
+ def test_register(self):
20
+ response = self.client.post('/api/auth/register/', self.user_data)
21
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
22
+ self.assertIn('access', response.data)
23
+ self.assertIn('refresh', response.data)
24
+
25
+ def test_login(self):
26
+ User.objects.create_user(
27
+ email='test@example.com',
28
+ password='TestPass123!',
29
+ first_name='Test',
30
+ )
31
+ response = self.client.post('/api/auth/login/', {
32
+ 'email': 'test@example.com',
33
+ 'password': 'TestPass123!',
34
+ })
35
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
36
+ self.assertIn('access', response.data)
37
+
38
+ def test_me(self):
39
+ user = User.objects.create_user(
40
+ email='test@example.com',
41
+ password='TestPass123!',
42
+ first_name='Test',
43
+ )
44
+ self.client.force_authenticate(user=user)
45
+ response = self.client.get('/api/auth/me/')
46
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
47
+ self.assertEqual(response.data['email'], 'test@example.com')
@@ -0,0 +1,10 @@
1
+ from django.urls import path, include
2
+ from rest_framework.routers import DefaultRouter
3
+ from .views import AuthViewSet
4
+
5
+ router = DefaultRouter()
6
+ router.register('', AuthViewSet, basename='auth')
7
+
8
+ urlpatterns = [
9
+ path('', include(router.urls)),
10
+ ]
@@ -0,0 +1,175 @@
1
+ from rest_framework import status
2
+ from rest_framework.decorators import action
3
+ from rest_framework.permissions import AllowAny, IsAuthenticated
4
+ from rest_framework.response import Response
5
+ from rest_framework.viewsets import ViewSet
6
+ from rest_framework_simplejwt.tokens import RefreshToken
7
+ from rest_framework_simplejwt.exceptions import TokenError
8
+ from drf_spectacular.utils import extend_schema
9
+
10
+ from .serializers import (
11
+ LoginSerializer,
12
+ RegisterSerializer,
13
+ UserSerializer,
14
+ TokenSerializer,
15
+ ChangePasswordSerializer,
16
+ ForgotPasswordSerializer,
17
+ ResetPasswordSerializer,
18
+ )
19
+
20
+
21
+ class AuthViewSet(ViewSet):
22
+ """Authentication endpoints for the application."""
23
+
24
+ @extend_schema(
25
+ request=LoginSerializer,
26
+ responses={200: TokenSerializer},
27
+ description='Authenticate with email and password. Returns JWT access and refresh tokens.',
28
+ tags=['auth'],
29
+ )
30
+ @action(detail=False, methods=['post'], permission_classes=[AllowAny])
31
+ def login(self, request):
32
+ serializer = LoginSerializer(data=request.data, context={'request': request})
33
+ serializer.is_valid(raise_exception=True)
34
+ user = serializer.validated_data['user']
35
+ refresh = RefreshToken.for_user(user)
36
+ return Response({
37
+ 'access': str(refresh.access_token),
38
+ 'refresh': str(refresh),
39
+ })
40
+
41
+ @extend_schema(
42
+ request=RegisterSerializer,
43
+ responses={201: TokenSerializer},
44
+ description='Create a new user account. Returns JWT tokens.',
45
+ tags=['auth'],
46
+ )
47
+ @action(detail=False, methods=['post'], permission_classes=[AllowAny])
48
+ def register(self, request):
49
+ serializer = RegisterSerializer(data=request.data)
50
+ serializer.is_valid(raise_exception=True)
51
+ user = serializer.save()
52
+ refresh = RefreshToken.for_user(user)
53
+ return Response(
54
+ {
55
+ 'access': str(refresh.access_token),
56
+ 'refresh': str(refresh),
57
+ },
58
+ status=status.HTTP_201_CREATED,
59
+ )
60
+
61
+ @extend_schema(
62
+ request=None,
63
+ responses={200: UserSerializer},
64
+ description='Get or update the current authenticated user profile.',
65
+ tags=['auth'],
66
+ )
67
+ @action(detail=False, methods=['get', 'patch'], permission_classes=[IsAuthenticated])
68
+ def me(self, request):
69
+ if request.method == 'GET':
70
+ serializer = UserSerializer(request.user)
71
+ return Response(serializer.data)
72
+
73
+ serializer = UserSerializer(request.user, data=request.data, partial=True)
74
+ serializer.is_valid(raise_exception=True)
75
+ serializer.save()
76
+ return Response(serializer.data)
77
+
78
+ @extend_schema(
79
+ request=ChangePasswordSerializer,
80
+ responses={200: None},
81
+ description='Change the current user password.',
82
+ tags=['auth'],
83
+ )
84
+ @action(
85
+ detail=False,
86
+ methods=['post'],
87
+ permission_classes=[IsAuthenticated],
88
+ url_path='change-password',
89
+ )
90
+ def change_password(self, request):
91
+ serializer = ChangePasswordSerializer(data=request.data, context={'request': request})
92
+ serializer.is_valid(raise_exception=True)
93
+ request.user.set_password(serializer.validated_data['new_password'])
94
+ request.user.save()
95
+ return Response({'detail': 'Password changed successfully.'})
96
+
97
+ @extend_schema(
98
+ request=ForgotPasswordSerializer,
99
+ responses={200: None},
100
+ description='Request a password reset email.',
101
+ tags=['auth'],
102
+ )
103
+ @action(
104
+ detail=False,
105
+ methods=['post'],
106
+ permission_classes=[AllowAny],
107
+ url_path='forgot-password',
108
+ )
109
+ def forgot_password(self, request):
110
+ serializer = ForgotPasswordSerializer(data=request.data)
111
+ serializer.is_valid(raise_exception=True)
112
+ # TODO: Implement email sending logic
113
+ # Always return success to prevent email enumeration
114
+ return Response({'detail': 'If an account exists, a reset email has been sent.'})
115
+
116
+ @extend_schema(
117
+ request=ResetPasswordSerializer,
118
+ responses={200: None},
119
+ description='Reset password using a token from the reset email.',
120
+ tags=['auth'],
121
+ )
122
+ @action(
123
+ detail=False,
124
+ methods=['post'],
125
+ permission_classes=[AllowAny],
126
+ url_path='reset-password',
127
+ )
128
+ def reset_password(self, request):
129
+ serializer = ResetPasswordSerializer(data=request.data)
130
+ serializer.is_valid(raise_exception=True)
131
+ # TODO: Implement token verification and password reset
132
+ return Response({'detail': 'Password has been reset successfully.'})
133
+
134
+ @extend_schema(
135
+ request=None,
136
+ responses={200: None},
137
+ description='Logout by blacklisting the refresh token.',
138
+ tags=['auth'],
139
+ )
140
+ @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
141
+ def logout(self, request):
142
+ try:
143
+ refresh_token = request.data.get('refresh')
144
+ if refresh_token:
145
+ token = RefreshToken(refresh_token)
146
+ token.blacklist()
147
+ except TokenError:
148
+ pass
149
+ return Response({'detail': 'Successfully logged out.'})
150
+
151
+ @extend_schema(
152
+ request=None,
153
+ responses={200: TokenSerializer},
154
+ description='Refresh the access token using a valid refresh token.',
155
+ tags=['auth'],
156
+ )
157
+ @action(detail=False, methods=['post'], permission_classes=[AllowAny])
158
+ def refresh(self, request):
159
+ refresh_token = request.data.get('refresh')
160
+ if not refresh_token:
161
+ return Response(
162
+ {'detail': 'Refresh token is required.'},
163
+ status=status.HTTP_400_BAD_REQUEST,
164
+ )
165
+ try:
166
+ token = RefreshToken(refresh_token)
167
+ return Response({
168
+ 'access': str(token.access_token),
169
+ 'refresh': str(token),
170
+ })
171
+ except TokenError:
172
+ return Response(
173
+ {'detail': 'Invalid or expired refresh token.'},
174
+ status=status.HTTP_401_UNAUTHORIZED,
175
+ )
File without changes
@@ -0,0 +1,9 @@
1
+ """
2
+ ASGI config for {{projectName}}.
3
+ """
4
+
5
+ import os
6
+ from django.core.asgi import get_asgi_application
7
+
8
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
9
+ application = get_asgi_application()
@@ -0,0 +1,13 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ env = os.environ.get('DJANGO_ENV', 'development')
7
+
8
+ from .base import * # noqa: F401, F403
9
+
10
+ if env == 'production':
11
+ from .production import * # noqa: F401, F403
12
+ else:
13
+ from .development import * # noqa: F401, F403
@@ -0,0 +1,117 @@
1
+ """
2
+ Base Django settings for {{projectName}}.
3
+ Generated by Blacksmith.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from datetime import timedelta
9
+
10
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
11
+
12
+ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-change-me')
13
+
14
+ INSTALLED_APPS = [
15
+ 'django.contrib.admin',
16
+ 'django.contrib.auth',
17
+ 'django.contrib.contenttypes',
18
+ 'django.contrib.sessions',
19
+ 'django.contrib.messages',
20
+ 'django.contrib.staticfiles',
21
+ # Third party
22
+ 'rest_framework',
23
+ 'drf_spectacular',
24
+ 'drf_spectacular_sidecar',
25
+ 'corsheaders',
26
+ 'rest_framework_simplejwt',
27
+ 'rest_framework_simplejwt.token_blacklist',
28
+ # Local apps
29
+ 'apps.users',
30
+ # blacksmith:apps
31
+ ]
32
+
33
+ MIDDLEWARE = [
34
+ 'django.middleware.security.SecurityMiddleware',
35
+ 'corsheaders.middleware.CorsMiddleware',
36
+ 'django.contrib.sessions.middleware.SessionMiddleware',
37
+ 'django.middleware.common.CommonMiddleware',
38
+ 'django.middleware.csrf.CsrfViewMiddleware',
39
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
40
+ 'django.contrib.messages.middleware.MessageMiddleware',
41
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
42
+ ]
43
+
44
+ ROOT_URLCONF = 'config.urls'
45
+
46
+ TEMPLATES = [
47
+ {
48
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
49
+ 'DIRS': [],
50
+ 'APP_DIRS': True,
51
+ 'OPTIONS': {
52
+ 'context_processors': [
53
+ 'django.template.context_processors.debug',
54
+ 'django.template.context_processors.request',
55
+ 'django.contrib.auth.context_processors.auth',
56
+ 'django.contrib.messages.context_processors.messages',
57
+ ],
58
+ },
59
+ },
60
+ ]
61
+
62
+ WSGI_APPLICATION = 'config.wsgi.application'
63
+
64
+ AUTH_PASSWORD_VALIDATORS = [
65
+ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
66
+ {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
67
+ {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
68
+ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
69
+ ]
70
+
71
+ LANGUAGE_CODE = 'en-us'
72
+ TIME_ZONE = 'UTC'
73
+ USE_I18N = True
74
+ USE_TZ = True
75
+
76
+ STATIC_URL = 'static/'
77
+ STATIC_ROOT = BASE_DIR / 'staticfiles'
78
+
79
+ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
80
+
81
+ # Custom user model
82
+ AUTH_USER_MODEL = 'users.CustomUser'
83
+
84
+ # Django REST Framework
85
+ REST_FRAMEWORK = {
86
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
87
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
88
+ 'rest_framework_simplejwt.authentication.JWTAuthentication',
89
+ ],
90
+ 'DEFAULT_PERMISSION_CLASSES': [
91
+ 'rest_framework.permissions.IsAuthenticated',
92
+ ],
93
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
94
+ 'PAGE_SIZE': 20,
95
+ }
96
+
97
+ # drf-spectacular (OpenAPI)
98
+ SPECTACULAR_SETTINGS = {
99
+ 'TITLE': '{{projectName}} API',
100
+ 'DESCRIPTION': 'API documentation for {{projectName}}. Generated by Blacksmith.',
101
+ 'VERSION': '1.0.0',
102
+ 'OAS_VERSION': '3.1.0',
103
+ 'SERVE_INCLUDE_SCHEMA': False,
104
+ 'COMPONENT_SPLIT_REQUEST': True,
105
+ 'SWAGGER_UI_DIST': 'SIDECAR',
106
+ 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
107
+ 'REDOC_DIST': 'SIDECAR',
108
+ }
109
+
110
+ # Simple JWT
111
+ SIMPLE_JWT = {
112
+ 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
113
+ 'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
114
+ 'ROTATE_REFRESH_TOKENS': True,
115
+ 'BLACKLIST_AFTER_ROTATION': True,
116
+ 'AUTH_HEADER_TYPES': ('Bearer',),
117
+ }
@@ -0,0 +1,19 @@
1
+ """
2
+ Development settings for {{projectName}}.
3
+ """
4
+
5
+ DEBUG = True
6
+
7
+ ALLOWED_HOSTS = ['*']
8
+
9
+ # SQLite for development
10
+ DATABASES = {
11
+ 'default': {
12
+ 'ENGINE': 'django.db.backends.sqlite3',
13
+ 'NAME': 'db.sqlite3',
14
+ }
15
+ }
16
+
17
+ # CORS - allow all origins in development
18
+ CORS_ALLOW_ALL_ORIGINS = True
19
+ CORS_ALLOW_CREDENTIALS = True
@@ -0,0 +1,31 @@
1
+ """
2
+ Production settings for {{projectName}}.
3
+ """
4
+
5
+ import os
6
+
7
+ DEBUG = False
8
+
9
+ ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')
10
+
11
+ # PostgreSQL for production
12
+ DATABASES = {
13
+ 'default': {
14
+ 'ENGINE': 'django.db.backends.postgresql',
15
+ 'NAME': os.environ.get('DB_NAME', '{{projectName}}'),
16
+ 'USER': os.environ.get('DB_USER', 'postgres'),
17
+ 'PASSWORD': os.environ.get('DB_PASSWORD', ''),
18
+ 'HOST': os.environ.get('DB_HOST', 'localhost'),
19
+ 'PORT': os.environ.get('DB_PORT', '5432'),
20
+ }
21
+ }
22
+
23
+ # CORS - restrict in production
24
+ CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', '').split(',')
25
+ CORS_ALLOW_CREDENTIALS = True
26
+
27
+ # Security
28
+ SECURE_BROWSER_XSS_FILTER = True
29
+ SECURE_CONTENT_TYPE_NOSNIFF = True
30
+ SESSION_COOKIE_SECURE = True
31
+ CSRF_COOKIE_SECURE = True