astris-python 0.1.0__py3-none-any.whl
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.
- astris/__init__.py +7 -0
- astris/assets/favicon.ico +0 -0
- astris/auth/__init__.py +29 -0
- astris/auth/installer.py +512 -0
- astris/auth/session.py +241 -0
- astris/cli.py +421 -0
- astris/config.py +50 -0
- astris/database/__init__.py +28 -0
- astris/database/migrations.py +259 -0
- astris/database/session.py +108 -0
- astris/http/__init__.py +25 -0
- astris/http/static.py +31 -0
- astris/inertia/__init__.py +8 -0
- astris/inertia/exceptions.py +133 -0
- astris/inertia/response.py +99 -0
- astris/inertia/shared.py +176 -0
- astris/inertia/vite.py +89 -0
- astris/installer.py +595 -0
- astris/kernel.py +230 -0
- astris/py.typed +0 -0
- astris/routing/__init__.py +31 -0
- astris/routing/router.py +38 -0
- astris/security/__init__.py +3 -0
- astris/security/csrf.py +100 -0
- astris_python-0.1.0.dist-info/METADATA +241 -0
- astris_python-0.1.0.dist-info/RECORD +29 -0
- astris_python-0.1.0.dist-info/WHEEL +4 -0
- astris_python-0.1.0.dist-info/entry_points.txt +5 -0
- astris_python-0.1.0.dist-info/licenses/LICENSE +21 -0
astris/__init__.py
ADDED
|
Binary file
|
astris/auth/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from astris.auth.session import (
|
|
2
|
+
AuthUser,
|
|
3
|
+
AuthUserId,
|
|
4
|
+
auth_required,
|
|
5
|
+
get_auth_user,
|
|
6
|
+
get_user_id,
|
|
7
|
+
guest_required,
|
|
8
|
+
hash_password,
|
|
9
|
+
is_authenticated,
|
|
10
|
+
login_user,
|
|
11
|
+
logout_user,
|
|
12
|
+
verify_and_update_password,
|
|
13
|
+
verify_password,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AuthUser",
|
|
18
|
+
"AuthUserId",
|
|
19
|
+
"auth_required",
|
|
20
|
+
"get_auth_user",
|
|
21
|
+
"get_user_id",
|
|
22
|
+
"guest_required",
|
|
23
|
+
"hash_password",
|
|
24
|
+
"is_authenticated",
|
|
25
|
+
"login_user",
|
|
26
|
+
"logout_user",
|
|
27
|
+
"verify_and_update_password",
|
|
28
|
+
"verify_password",
|
|
29
|
+
]
|
astris/auth/installer.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
AUTH_MODEL_TEMPLATE = """from astris.database import Field, SQLModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UserBase(SQLModel):
|
|
7
|
+
name: str = Field(index=True)
|
|
8
|
+
email: str = Field(unique=True, index=True)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class User(UserBase, table=True):
|
|
12
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
13
|
+
hashed_password: str = Field()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserLogin(SQLModel):
|
|
17
|
+
email: str
|
|
18
|
+
password: str
|
|
19
|
+
remember: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserRegister(SQLModel):
|
|
23
|
+
name: str
|
|
24
|
+
email: str
|
|
25
|
+
password: str
|
|
26
|
+
password_confirmation: str
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
AUTH_SERVICE_TEMPLATE = """from app.modules.auth.auth_model import User, UserRegister
|
|
30
|
+
from astris.auth import hash_password, verify_and_update_password
|
|
31
|
+
from astris.database import Session, select
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthService:
|
|
35
|
+
@staticmethod
|
|
36
|
+
def authenticate(session: Session, email: str, password: str) -> User | None:
|
|
37
|
+
user = session.exec(select(User).where(User.email == email)).first()
|
|
38
|
+
if not user:
|
|
39
|
+
return None
|
|
40
|
+
valid, updated_hash = verify_and_update_password(password, user.hashed_password)
|
|
41
|
+
if not valid:
|
|
42
|
+
return None
|
|
43
|
+
if updated_hash:
|
|
44
|
+
user.hashed_password = updated_hash
|
|
45
|
+
session.add(user)
|
|
46
|
+
session.commit()
|
|
47
|
+
session.refresh(user)
|
|
48
|
+
return user
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def register(session: Session, data: UserRegister) -> User:
|
|
52
|
+
hashed = hash_password(data.password)
|
|
53
|
+
user = User(
|
|
54
|
+
name=data.name,
|
|
55
|
+
email=data.email,
|
|
56
|
+
hashed_password=hashed,
|
|
57
|
+
)
|
|
58
|
+
session.add(user)
|
|
59
|
+
session.commit()
|
|
60
|
+
session.refresh(user)
|
|
61
|
+
return user
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def email_exists(session: Session, email: str) -> bool:
|
|
65
|
+
return session.exec(select(User).where(User.email == email)).first() is not None
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
AUTH_CONTROLLER_TEMPLATE = """from app.modules.auth.auth_model import UserLogin, UserRegister
|
|
69
|
+
from app.modules.auth.auth_service import AuthService
|
|
70
|
+
from astris.auth import auth_required, guest_required, login_user, logout_user
|
|
71
|
+
from astris.database import DatabaseSession
|
|
72
|
+
from astris.http import HTTPException, RedirectResponse, Request
|
|
73
|
+
from astris.inertia import InertiaResponse, flash
|
|
74
|
+
from astris.routing import Controller
|
|
75
|
+
|
|
76
|
+
controller = Controller(tags=["Auth"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@controller.get("/login", dependencies=[guest_required])
|
|
80
|
+
async def login_page(request: Request) -> InertiaResponse:
|
|
81
|
+
return InertiaResponse(request, "Auth/Login")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@controller.post("/login", dependencies=[guest_required])
|
|
85
|
+
async def login(
|
|
86
|
+
request: Request, data: UserLogin, db: DatabaseSession
|
|
87
|
+
) -> RedirectResponse:
|
|
88
|
+
user = AuthService.authenticate(db, data.email, data.password)
|
|
89
|
+
if not user:
|
|
90
|
+
raise HTTPException(
|
|
91
|
+
status_code=422,
|
|
92
|
+
detail={"error": "These credentials do not match our records."},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
login_user(request, user)
|
|
96
|
+
flash(request, "success", f"Welcome back, {user.name}!")
|
|
97
|
+
return RedirectResponse(url="/dashboard", status_code=303)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@controller.get("/register", dependencies=[guest_required])
|
|
101
|
+
async def register_page(request: Request) -> InertiaResponse:
|
|
102
|
+
return InertiaResponse(request, "Auth/Register")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@controller.post("/register", dependencies=[guest_required])
|
|
106
|
+
async def register(
|
|
107
|
+
request: Request, data: UserRegister, db: DatabaseSession
|
|
108
|
+
) -> RedirectResponse:
|
|
109
|
+
if data.password != data.password_confirmation:
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=422,
|
|
112
|
+
detail={
|
|
113
|
+
"password_confirmation": "The password confirmation does not match."
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if AuthService.email_exists(db, data.email):
|
|
118
|
+
raise HTTPException(
|
|
119
|
+
status_code=422,
|
|
120
|
+
detail={"email": "An account with this email already exists."},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
user = AuthService.register(db, data)
|
|
124
|
+
login_user(request, user)
|
|
125
|
+
flash(request, "success", f"Welcome to Astris, {user.name}!")
|
|
126
|
+
return RedirectResponse(url="/dashboard", status_code=303)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@controller.post("/logout")
|
|
130
|
+
async def logout(request: Request) -> RedirectResponse:
|
|
131
|
+
logout_user(request)
|
|
132
|
+
flash(request, "info", "You have been logged out.")
|
|
133
|
+
return RedirectResponse(url="/login", status_code=303)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@controller.get("/dashboard", dependencies=[auth_required])
|
|
137
|
+
async def dashboard(request: Request) -> InertiaResponse:
|
|
138
|
+
return InertiaResponse(request, "Dashboard")
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
ASTRIS_LOGO_VUE_TEMPLATE = """<template>
|
|
142
|
+
<svg
|
|
143
|
+
viewBox="0 0 792 792"
|
|
144
|
+
fill="currentColor"
|
|
145
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
146
|
+
>
|
|
147
|
+
<path d="m50 757h88.8l37.8-92.3c-31.4-1.6-59-6.3-82.4-14.2z"/>
|
|
148
|
+
<path d="m596.3 550.8q-11 6.5-22.3 12.6l79.1 193.6h88.9l-98.1-236.5q-22.4 15.6-47.6 30.3z"/>
|
|
149
|
+
<path d="m210.1 582.6l185.9-454.7 147.5 360.9c25-14.4 48-29.9 68.2-46l-169.2-407.8h-93l-221.6 534.1c21.2 8.3 49.5 12.9 82.2 13.5z"/>
|
|
150
|
+
<path fill-rule="evenodd" d="m372.5 466.8l23.5 76.4 23.5-76.4 69-23.5-68.8-23.8-23.7-75.8-23.7 75.8-68.8 23.8 69 23.5z"/>
|
|
151
|
+
<path d="m756.2 309.9c-18.3-50.9-96.5-72.1-201.4-61.6 81.6-3.1 141.7 15.8 156.8 58 26.6 74.2-96.3 192.3-273.9 256.1-177.7 63.7-343.3 55.2-370-19-15.5-43.4 19.7-99.6 87.1-151.5-90.4 60.9-137.6 131.1-118.9 183.2 29.4 82 214.3 90.6 413.1 19.3 198.8-71.3 336.6-202.5 307.2-284.5z"/>
|
|
152
|
+
<path d="m90 533.2c2.1 6 5.3 11.5 9.4 16.6q-2.3-3.9-3.9-8.2c-12.7-35.5 18.2-82.5 77.1-127.1l12.8-31c-71.7 50.7-110.7 107.1-95.4 149.7z"/>
|
|
153
|
+
<path d="m437 275.7c-30 6.8-61.1 15.9-92.6 27.2q-0.3 0.1-0.6 0.2l-10.1 24.7q8.5-3.3 17.3-6.4c31.8-11.4 63.2-20.7 93.3-27.8l-7.4-17.9z"/>
|
|
154
|
+
<path d="m688.7 323.7q1.4 4.1 2.1 8.4c0-6.6-1.1-12.9-3.2-18.9-13.1-36.3-63.2-53.7-132.2-52.9l7 16.8c66.6-1.8 114.4 13.4 126.3 46.6z"/>
|
|
155
|
+
</svg>
|
|
156
|
+
</template>
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
VUE_LOGIN_TEMPLATE = """<script setup lang="ts">
|
|
160
|
+
import { useForm, Link } from '@inertiajs/vue3'
|
|
161
|
+
import AstrisLogo from '../../Components/AstrisLogo.vue'
|
|
162
|
+
|
|
163
|
+
const form = useForm({
|
|
164
|
+
email: '',
|
|
165
|
+
password: '',
|
|
166
|
+
remember: false,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const submit = () => {
|
|
170
|
+
form.post('/login', {
|
|
171
|
+
onFinish: () => form.reset('password'),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<template>
|
|
177
|
+
<div class="min-h-screen bg-slate-950 flex flex-col justify-center py-12 sm:px-6 lg:px-8 text-slate-100 font-sans relative selection:bg-sky-500 selection:text-white">
|
|
178
|
+
<!-- Subtle Background Glow -->
|
|
179
|
+
<div class="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[300px] bg-sky-500/10 blur-[100px] rounded-full pointer-events-none -z-10"></div>
|
|
180
|
+
|
|
181
|
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
|
182
|
+
<div class="flex justify-center">
|
|
183
|
+
<Link href="/" class="p-3 rounded-2xl bg-slate-900/80 border border-slate-800 backdrop-blur shadow-xl hover:border-sky-500/40 transition duration-200">
|
|
184
|
+
<AstrisLogo class="w-10 h-10 text-sky-400" />
|
|
185
|
+
</Link>
|
|
186
|
+
</div>
|
|
187
|
+
<h2 class="mt-4 text-center text-3xl font-extrabold tracking-tight text-white">
|
|
188
|
+
Sign in to your account
|
|
189
|
+
</h2>
|
|
190
|
+
<p class="mt-2 text-center text-sm text-slate-400">
|
|
191
|
+
Or
|
|
192
|
+
<Link href="/register" class="font-medium text-sky-400 hover:text-sky-300 underline underline-offset-4 transition">
|
|
193
|
+
create a new account
|
|
194
|
+
</Link>
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
199
|
+
<div class="bg-slate-900/80 backdrop-blur border border-slate-800 py-8 px-6 shadow-2xl rounded-2xl sm:px-10">
|
|
200
|
+
<form @submit.prevent="submit" class="space-y-6">
|
|
201
|
+
<div
|
|
202
|
+
v-if="form.errors.error"
|
|
203
|
+
class="p-3 bg-rose-500/10 border border-rose-500/30 rounded-xl text-sm text-rose-400 flex items-center gap-2"
|
|
204
|
+
>
|
|
205
|
+
<svg class="w-4 h-4 shrink-0 text-rose-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
206
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
207
|
+
</svg>
|
|
208
|
+
<span>{{ form.errors.error }}</span>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div>
|
|
212
|
+
<label for="email" class="block text-sm font-medium text-slate-300">Email address</label>
|
|
213
|
+
<div class="mt-1">
|
|
214
|
+
<input
|
|
215
|
+
id="email"
|
|
216
|
+
v-model="form.email"
|
|
217
|
+
type="email"
|
|
218
|
+
autocomplete="email"
|
|
219
|
+
required
|
|
220
|
+
class="appearance-none block w-full px-3.5 py-2.5 border border-slate-700 rounded-xl shadow-sm placeholder-slate-500 bg-slate-800/80 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition text-sm"
|
|
221
|
+
placeholder="you@example.com"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<p v-if="form.errors.email" class="mt-2 text-sm text-rose-400">{{ form.errors.email }}</p>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div>
|
|
228
|
+
<label for="password" class="block text-sm font-medium text-slate-300">Password</label>
|
|
229
|
+
<div class="mt-1">
|
|
230
|
+
<input
|
|
231
|
+
id="password"
|
|
232
|
+
v-model="form.password"
|
|
233
|
+
type="password"
|
|
234
|
+
autocomplete="current-password"
|
|
235
|
+
required
|
|
236
|
+
class="appearance-none block w-full px-3.5 py-2.5 border border-slate-700 rounded-xl shadow-sm placeholder-slate-500 bg-slate-800/80 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition text-sm"
|
|
237
|
+
placeholder="••••••••"
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
<p v-if="form.errors.password" class="mt-2 text-sm text-rose-400">{{ form.errors.password }}</p>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="flex items-center justify-between">
|
|
244
|
+
<div class="flex items-center">
|
|
245
|
+
<input
|
|
246
|
+
id="remember"
|
|
247
|
+
v-model="form.remember"
|
|
248
|
+
type="checkbox"
|
|
249
|
+
class="h-4 w-4 text-sky-500 focus:ring-sky-400 border-slate-700 rounded bg-slate-800"
|
|
250
|
+
/>
|
|
251
|
+
<label for="remember" class="ml-2 block text-sm text-slate-400">Remember me</label>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div>
|
|
256
|
+
<button
|
|
257
|
+
type="submit"
|
|
258
|
+
:disabled="form.processing"
|
|
259
|
+
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-xl shadow-md text-sm font-semibold text-white bg-sky-500 hover:bg-sky-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 disabled:opacity-50 transition duration-200"
|
|
260
|
+
>
|
|
261
|
+
<span v-if="form.processing">Signing in...</span>
|
|
262
|
+
<span v-else>Sign In</span>
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
</form>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</template>
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
VUE_REGISTER_TEMPLATE = """<script setup lang="ts">
|
|
273
|
+
import { useForm, Link } from '@inertiajs/vue3'
|
|
274
|
+
import AstrisLogo from '../../Components/AstrisLogo.vue'
|
|
275
|
+
|
|
276
|
+
const form = useForm({
|
|
277
|
+
name: '',
|
|
278
|
+
email: '',
|
|
279
|
+
password: '',
|
|
280
|
+
password_confirmation: '',
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const submit = () => {
|
|
284
|
+
form.post('/register', {
|
|
285
|
+
onFinish: () => form.reset('password', 'password_confirmation'),
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
</script>
|
|
289
|
+
|
|
290
|
+
<template>
|
|
291
|
+
<div class="min-h-screen bg-slate-950 flex flex-col justify-center py-12 sm:px-6 lg:px-8 text-slate-100 font-sans relative selection:bg-sky-500 selection:text-white">
|
|
292
|
+
<!-- Subtle Background Glow -->
|
|
293
|
+
<div class="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[300px] bg-sky-500/10 blur-[100px] rounded-full pointer-events-none -z-10"></div>
|
|
294
|
+
|
|
295
|
+
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
|
296
|
+
<div class="flex justify-center">
|
|
297
|
+
<Link href="/" class="p-3 rounded-2xl bg-slate-900/80 border border-slate-800 backdrop-blur shadow-xl hover:border-sky-500/40 transition duration-200">
|
|
298
|
+
<AstrisLogo class="w-10 h-10 text-sky-400" />
|
|
299
|
+
</Link>
|
|
300
|
+
</div>
|
|
301
|
+
<h2 class="mt-4 text-center text-3xl font-extrabold tracking-tight text-white">
|
|
302
|
+
Create a new account
|
|
303
|
+
</h2>
|
|
304
|
+
<p class="mt-2 text-center text-sm text-slate-400">
|
|
305
|
+
Already have an account?
|
|
306
|
+
<Link href="/login" class="font-medium text-sky-400 hover:text-sky-300 underline underline-offset-4 transition">
|
|
307
|
+
Sign in
|
|
308
|
+
</Link>
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
313
|
+
<div class="bg-slate-900/80 backdrop-blur border border-slate-800 py-8 px-6 shadow-2xl rounded-2xl sm:px-10">
|
|
314
|
+
<form @submit.prevent="submit" class="space-y-5">
|
|
315
|
+
<div>
|
|
316
|
+
<label for="name" class="block text-sm font-medium text-slate-300">Full Name</label>
|
|
317
|
+
<div class="mt-1">
|
|
318
|
+
<input
|
|
319
|
+
id="name"
|
|
320
|
+
v-model="form.name"
|
|
321
|
+
type="text"
|
|
322
|
+
autocomplete="name"
|
|
323
|
+
required
|
|
324
|
+
class="appearance-none block w-full px-3.5 py-2.5 border border-slate-700 rounded-xl shadow-sm placeholder-slate-500 bg-slate-800/80 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition text-sm"
|
|
325
|
+
placeholder="Jane Doe"
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
<p v-if="form.errors.name" class="mt-2 text-sm text-rose-400">{{ form.errors.name }}</p>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div>
|
|
332
|
+
<label for="email" class="block text-sm font-medium text-slate-300">Email address</label>
|
|
333
|
+
<div class="mt-1">
|
|
334
|
+
<input
|
|
335
|
+
id="email"
|
|
336
|
+
v-model="form.email"
|
|
337
|
+
type="email"
|
|
338
|
+
autocomplete="email"
|
|
339
|
+
required
|
|
340
|
+
class="appearance-none block w-full px-3.5 py-2.5 border border-slate-700 rounded-xl shadow-sm placeholder-slate-500 bg-slate-800/80 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition text-sm"
|
|
341
|
+
placeholder="you@example.com"
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
<p v-if="form.errors.email" class="mt-2 text-sm text-rose-400">{{ form.errors.email }}</p>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div>
|
|
348
|
+
<label for="password" class="block text-sm font-medium text-slate-300">Password</label>
|
|
349
|
+
<div class="mt-1">
|
|
350
|
+
<input
|
|
351
|
+
id="password"
|
|
352
|
+
v-model="form.password"
|
|
353
|
+
type="password"
|
|
354
|
+
autocomplete="new-password"
|
|
355
|
+
required
|
|
356
|
+
class="appearance-none block w-full px-3.5 py-2.5 border border-slate-700 rounded-xl shadow-sm placeholder-slate-500 bg-slate-800/80 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition text-sm"
|
|
357
|
+
placeholder="••••••••"
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
<p v-if="form.errors.password" class="mt-2 text-sm text-rose-400">{{ form.errors.password }}</p>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div>
|
|
364
|
+
<label for="password_confirmation" class="block text-sm font-medium text-slate-300">Confirm Password</label>
|
|
365
|
+
<div class="mt-1">
|
|
366
|
+
<input
|
|
367
|
+
id="password_confirmation"
|
|
368
|
+
v-model="form.password_confirmation"
|
|
369
|
+
type="password"
|
|
370
|
+
autocomplete="new-password"
|
|
371
|
+
required
|
|
372
|
+
class="appearance-none block w-full px-3.5 py-2.5 border border-slate-700 rounded-xl shadow-sm placeholder-slate-500 bg-slate-800/80 text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition text-sm"
|
|
373
|
+
placeholder="••••••••"
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
<p v-if="form.errors.password_confirmation" class="mt-2 text-sm text-rose-400">{{ form.errors.password_confirmation }}</p>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div>
|
|
380
|
+
<button
|
|
381
|
+
type="submit"
|
|
382
|
+
:disabled="form.processing"
|
|
383
|
+
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-xl shadow-md text-sm font-semibold text-white bg-sky-500 hover:bg-sky-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 disabled:opacity-50 transition duration-200"
|
|
384
|
+
>
|
|
385
|
+
<span v-if="form.processing">Creating account...</span>
|
|
386
|
+
<span v-else>Register</span>
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
</form>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</template>
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
VUE_DASHBOARD_TEMPLATE = """<script setup lang="ts">
|
|
397
|
+
import { usePage, router, Link } from '@inertiajs/vue3'
|
|
398
|
+
import AstrisLogo from '../Components/AstrisLogo.vue'
|
|
399
|
+
|
|
400
|
+
const page = usePage()
|
|
401
|
+
|
|
402
|
+
const logout = () => {
|
|
403
|
+
router.post('/logout')
|
|
404
|
+
}
|
|
405
|
+
</script>
|
|
406
|
+
|
|
407
|
+
<template>
|
|
408
|
+
<div class="min-h-screen bg-slate-950 text-slate-100 font-sans relative selection:bg-sky-500 selection:text-white">
|
|
409
|
+
<!-- Subtle Background Ambient Glow -->
|
|
410
|
+
<div class="absolute top-0 right-1/4 w-[500px] h-[250px] bg-sky-500/10 blur-[120px] rounded-full pointer-events-none -z-10"></div>
|
|
411
|
+
|
|
412
|
+
<nav class="border-b border-slate-800 bg-slate-900/60 backdrop-blur sticky top-0 z-50">
|
|
413
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
414
|
+
<div class="flex justify-between h-16 items-center">
|
|
415
|
+
<Link href="/" class="flex items-center space-x-3 group">
|
|
416
|
+
<AstrisLogo class="w-8 h-8 text-sky-400 group-hover:scale-105 transition duration-200" />
|
|
417
|
+
<span class="font-bold text-lg text-white">Astris App</span>
|
|
418
|
+
</Link>
|
|
419
|
+
<div class="flex items-center space-x-4">
|
|
420
|
+
<div class="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-slate-800/60 border border-slate-700/60 text-xs font-medium text-slate-300">
|
|
421
|
+
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
|
422
|
+
{{ page.props.auth?.user?.name || page.props.auth?.user?.email }}
|
|
423
|
+
</div>
|
|
424
|
+
<button
|
|
425
|
+
@click="logout"
|
|
426
|
+
class="px-3.5 py-1.5 rounded-xl text-xs font-medium text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 border border-slate-700 transition duration-200"
|
|
427
|
+
>
|
|
428
|
+
Sign Out
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</nav>
|
|
434
|
+
|
|
435
|
+
<main class="max-w-7xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
|
|
436
|
+
<div v-if="page.props.flash?.success" class="mb-6 p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-sm flex items-center gap-2.5">
|
|
437
|
+
<svg class="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
438
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
439
|
+
</svg>
|
|
440
|
+
<span>{{ page.props.flash.success }}</span>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<div class="bg-slate-900/80 border border-slate-800 rounded-3xl p-8 sm:p-10 shadow-2xl backdrop-blur">
|
|
444
|
+
<h1 class="text-3xl font-extrabold text-white">Dashboard</h1>
|
|
445
|
+
<p class="mt-2 text-slate-400">
|
|
446
|
+
Welcome to your authenticated Astris application!
|
|
447
|
+
</p>
|
|
448
|
+
|
|
449
|
+
<div class="mt-8 grid grid-cols-1 md:grid-cols-3 gap-5">
|
|
450
|
+
<div class="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/60 shadow-sm">
|
|
451
|
+
<div class="text-xs font-medium text-slate-400 uppercase tracking-wider">Session Status</div>
|
|
452
|
+
<div class="mt-2 text-xl font-bold text-emerald-400 flex items-center gap-2">
|
|
453
|
+
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400 shadow-[0_0_8px_#34d399]"></span>
|
|
454
|
+
Authenticated
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/60 shadow-sm">
|
|
458
|
+
<div class="text-xs font-medium text-slate-400 uppercase tracking-wider">User Email</div>
|
|
459
|
+
<div class="mt-2 text-base font-semibold text-white truncate">{{ page.props.auth?.user?.email }}</div>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="p-6 rounded-2xl bg-slate-800/50 border border-slate-700/60 shadow-sm">
|
|
462
|
+
<div class="text-xs font-medium text-slate-400 uppercase tracking-wider">Security Engine</div>
|
|
463
|
+
<div class="mt-2 text-base font-semibold text-sky-400 flex items-center gap-2">
|
|
464
|
+
<span>🛡️</span>
|
|
465
|
+
Signed Cookies & CSRF
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</main>
|
|
471
|
+
</div>
|
|
472
|
+
</template>
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def install_auth_starter(base_path: Path | None = None) -> None:
|
|
477
|
+
"""Install a full-stack authentication starter kit (models, services, controllers, and Vue pages)."""
|
|
478
|
+
root = base_path or Path.cwd()
|
|
479
|
+
|
|
480
|
+
# 1. Backend module: app/modules/auth/
|
|
481
|
+
auth_module_dir = root / "app" / "modules" / "auth"
|
|
482
|
+
auth_module_dir.mkdir(parents=True, exist_ok=True)
|
|
483
|
+
(auth_module_dir / "__init__.py").touch()
|
|
484
|
+
|
|
485
|
+
(auth_module_dir / "auth_model.py").write_text(
|
|
486
|
+
AUTH_MODEL_TEMPLATE, encoding="utf-8"
|
|
487
|
+
)
|
|
488
|
+
(auth_module_dir / "auth_service.py").write_text(
|
|
489
|
+
AUTH_SERVICE_TEMPLATE, encoding="utf-8"
|
|
490
|
+
)
|
|
491
|
+
(auth_module_dir / "auth_controller.py").write_text(
|
|
492
|
+
AUTH_CONTROLLER_TEMPLATE, encoding="utf-8"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# 2. Components: resources/js/Components/AstrisLogo.vue
|
|
496
|
+
components_dir = root / "resources" / "js" / "Components"
|
|
497
|
+
components_dir.mkdir(parents=True, exist_ok=True)
|
|
498
|
+
logo_file = components_dir / "AstrisLogo.vue"
|
|
499
|
+
if not logo_file.exists():
|
|
500
|
+
logo_file.write_text(ASTRIS_LOGO_VUE_TEMPLATE, encoding="utf-8")
|
|
501
|
+
|
|
502
|
+
# 3. Frontend pages: resources/js/Pages/Auth/
|
|
503
|
+
auth_pages_dir = root / "resources" / "js" / "Pages" / "Auth"
|
|
504
|
+
auth_pages_dir.mkdir(parents=True, exist_ok=True)
|
|
505
|
+
|
|
506
|
+
(auth_pages_dir / "Login.vue").write_text(VUE_LOGIN_TEMPLATE, encoding="utf-8")
|
|
507
|
+
(auth_pages_dir / "Register.vue").write_text(
|
|
508
|
+
VUE_REGISTER_TEMPLATE, encoding="utf-8"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
pages_dir = root / "resources" / "js" / "Pages"
|
|
512
|
+
(pages_dir / "Dashboard.vue").write_text(VUE_DASHBOARD_TEMPLATE, encoding="utf-8")
|