create-nara 1.0.8 → 1.0.10
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.
Potentially problematic release.
This version of create-nara might be problematic. Click here for more details.
- package/package.json +1 -1
- package/templates/features/auth/app/controllers/AuthController.ts +9 -10
- package/templates/features/auth/app/middlewares/auth.ts +2 -2
- package/templates/svelte/resources/js/pages/auth/forgot-password.svelte +32 -17
- package/templates/svelte/resources/js/pages/auth/login.svelte +25 -2
- package/templates/svelte/resources/js/pages/auth/register.svelte +33 -14
- package/templates/svelte/resources/js/pages/auth/reset-password.svelte +40 -17
package/package.json
CHANGED
|
@@ -4,14 +4,13 @@ import bcrypt from 'bcrypt';
|
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
5
|
|
|
6
6
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
7
|
-
const
|
|
7
|
+
const JWT_EXPIRES_SECONDS = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
8
8
|
|
|
9
9
|
// Cookie options for auth token
|
|
10
10
|
const COOKIE_OPTIONS = {
|
|
11
11
|
httpOnly: true,
|
|
12
12
|
secure: process.env.NODE_ENV === 'production',
|
|
13
13
|
sameSite: 'lax' as const,
|
|
14
|
-
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
|
|
15
14
|
path: '/',
|
|
16
15
|
};
|
|
17
16
|
|
|
@@ -33,11 +32,11 @@ export class AuthController extends BaseController {
|
|
|
33
32
|
const token = jwt.sign(
|
|
34
33
|
{ userId: 1, email, name: 'Demo User' },
|
|
35
34
|
JWT_SECRET,
|
|
36
|
-
{ expiresIn:
|
|
35
|
+
{ expiresIn: JWT_EXPIRES_SECONDS }
|
|
37
36
|
);
|
|
38
37
|
|
|
39
|
-
// Set auth cookie for web routes
|
|
40
|
-
res.cookie('auth_token', token, COOKIE_OPTIONS);
|
|
38
|
+
// Set auth cookie for web routes (maxAge in ms)
|
|
39
|
+
res.cookie('auth_token', token, JWT_EXPIRES_SECONDS * 1000, COOKIE_OPTIONS);
|
|
41
40
|
|
|
42
41
|
return jsonSuccess(res, {
|
|
43
42
|
user: { id: 1, email, name: 'Demo User' },
|
|
@@ -66,11 +65,11 @@ export class AuthController extends BaseController {
|
|
|
66
65
|
const token = jwt.sign(
|
|
67
66
|
{ userId: 1, email, name },
|
|
68
67
|
JWT_SECRET,
|
|
69
|
-
{ expiresIn:
|
|
68
|
+
{ expiresIn: JWT_EXPIRES_SECONDS }
|
|
70
69
|
);
|
|
71
70
|
|
|
72
|
-
// Set auth cookie for web routes
|
|
73
|
-
res.cookie('auth_token', token, COOKIE_OPTIONS);
|
|
71
|
+
// Set auth cookie for web routes (maxAge in ms)
|
|
72
|
+
res.cookie('auth_token', token, JWT_EXPIRES_SECONDS * 1000, COOKIE_OPTIONS);
|
|
74
73
|
|
|
75
74
|
return jsonSuccess(res, {
|
|
76
75
|
user: { id: 1, email, name },
|
|
@@ -89,8 +88,8 @@ export class AuthController extends BaseController {
|
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
async logout(req: NaraRequest, res: NaraResponse) {
|
|
92
|
-
// Clear auth cookie
|
|
93
|
-
res.cookie('auth_token', '',
|
|
91
|
+
// Clear auth cookie (set maxAge to 0)
|
|
92
|
+
res.cookie('auth_token', '', 0, COOKIE_OPTIONS);
|
|
94
93
|
|
|
95
94
|
return jsonSuccess(res, { redirect: '/login' }, 'Logged out successfully');
|
|
96
95
|
}
|
|
@@ -49,7 +49,7 @@ export function webAuthMiddleware(req: NaraRequest, res: NaraResponse, next: ()
|
|
|
49
49
|
next();
|
|
50
50
|
} catch (error) {
|
|
51
51
|
// Clear invalid token
|
|
52
|
-
res.cookie('auth_token', '',
|
|
52
|
+
res.cookie('auth_token', '', 0);
|
|
53
53
|
if (req.headers['x-inertia']) {
|
|
54
54
|
res.status(409).setHeader('X-Inertia-Location', '/login').send('');
|
|
55
55
|
} else {
|
|
@@ -77,7 +77,7 @@ export function guestMiddleware(req: NaraRequest, res: NaraResponse, next: () =>
|
|
|
77
77
|
return;
|
|
78
78
|
} catch {
|
|
79
79
|
// Invalid token, clear it and continue
|
|
80
|
-
res.cookie('auth_token', '',
|
|
80
|
+
res.cookie('auth_token', '', 0);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
import { inertia, router } from "@inertiajs/svelte";
|
|
3
3
|
import NaraIcon from "../../components/NaraIcon.svelte";
|
|
4
4
|
import DarkModeToggle from "../../components/DarkModeToggle.svelte";
|
|
5
|
-
import
|
|
6
|
-
import { api, Toast } from "../../components/helper";
|
|
5
|
+
import { Toast } from "../../components/helper";
|
|
7
6
|
|
|
8
7
|
interface ForgotPasswordForm {
|
|
9
8
|
email: string;
|
|
10
|
-
phone: string;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
let form: ForgotPasswordForm = {
|
|
14
12
|
email: "",
|
|
15
|
-
phone: "",
|
|
16
13
|
};
|
|
17
14
|
|
|
18
15
|
let success: boolean = $state(false);
|
|
16
|
+
let loading: boolean = $state(false);
|
|
19
17
|
let { error }: { error?: string } = $props();
|
|
20
18
|
|
|
21
19
|
$effect(() => {
|
|
@@ -23,12 +21,29 @@
|
|
|
23
21
|
});
|
|
24
22
|
|
|
25
23
|
async function submitForm(): Promise<void> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
loading = true;
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch('/api/auth/forgot-password', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ email: form.email })
|
|
30
|
+
});
|
|
31
|
+
const result = await response.json();
|
|
32
|
+
|
|
33
|
+
if (result.success) {
|
|
34
|
+
success = true;
|
|
35
|
+
form.email = "";
|
|
36
|
+
Toast(result.message || 'Reset link sent!', 'success');
|
|
37
|
+
} else {
|
|
38
|
+
const errorMsg = result.errors
|
|
39
|
+
? Object.values(result.errors).flat().join(', ')
|
|
40
|
+
: result.message || 'Failed to send reset link';
|
|
41
|
+
Toast(errorMsg, 'error');
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
Toast('An error occurred. Please try again.', 'error');
|
|
45
|
+
} finally {
|
|
46
|
+
loading = false;
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
49
|
</script>
|
|
@@ -62,7 +77,7 @@
|
|
|
62
77
|
|
|
63
78
|
{#if success}
|
|
64
79
|
<div class="p-4 mb-4 text-sm text-green-400 rounded-lg bg-green-900/50" role="alert">
|
|
65
|
-
|
|
80
|
+
Password reset link has been sent to your email.
|
|
66
81
|
</div>
|
|
67
82
|
{/if}
|
|
68
83
|
|
|
@@ -71,24 +86,24 @@
|
|
|
71
86
|
on:submit|preventDefault={submitForm}
|
|
72
87
|
>
|
|
73
88
|
<div>
|
|
74
|
-
<label for="email" class="block mb-2 text-sm font-medium text-slate-200">Email
|
|
89
|
+
<label for="email" class="block mb-2 text-sm font-medium text-slate-200">Email</label>
|
|
75
90
|
<input
|
|
76
91
|
bind:value={form.email}
|
|
77
|
-
type="
|
|
92
|
+
type="email"
|
|
78
93
|
name="email"
|
|
79
94
|
id="email"
|
|
80
95
|
class="bg-slate-900/70 border border-slate-700 text-slate-50 sm:text-sm rounded-lg focus:ring-2 focus:ring-primary-400 focus:border-primary-400 focus:outline-none block w-full py-2.5 px-3 placeholder-slate-500"
|
|
81
|
-
placeholder="email@example.com
|
|
96
|
+
placeholder="email@example.com"
|
|
82
97
|
required
|
|
83
98
|
/>
|
|
84
99
|
</div>
|
|
85
100
|
|
|
86
|
-
<button type="submit" class="w-full text-sm font-medium rounded-full px-5 py-2.5 text-slate-950 bg-primary-400 hover:bg-primary-300 focus:ring-4 focus:outline-none focus:ring-primary-300">
|
|
87
|
-
|
|
101
|
+
<button type="submit" disabled={loading} class="w-full text-sm font-medium rounded-full px-5 py-2.5 text-slate-950 bg-primary-400 hover:bg-primary-300 focus:ring-4 focus:outline-none focus:ring-primary-300 disabled:opacity-50">
|
|
102
|
+
{loading ? 'Sending...' : 'Send Reset Link'}
|
|
88
103
|
</button>
|
|
89
104
|
|
|
90
105
|
<p class="text-sm font-light text-slate-400">
|
|
91
|
-
|
|
106
|
+
Remember your password? <a href="/login" use:inertia class="font-medium text-primary-400 hover:underline">Login here</a>
|
|
92
107
|
</p>
|
|
93
108
|
</form>
|
|
94
109
|
</div>
|
|
@@ -19,14 +19,37 @@
|
|
|
19
19
|
password: '',
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
let loading = $state(false);
|
|
22
23
|
let { error }: { error?: string } = $props();
|
|
23
24
|
|
|
24
25
|
$effect(() => {
|
|
25
26
|
if (error) Toast(error, 'error');
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
function submitForm(): void {
|
|
29
|
-
|
|
29
|
+
async function submitForm(): Promise<void> {
|
|
30
|
+
loading = true;
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch('/api/auth/login', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ email: form.email, password: form.password })
|
|
36
|
+
});
|
|
37
|
+
const result = await response.json();
|
|
38
|
+
|
|
39
|
+
if (result.success) {
|
|
40
|
+
Toast(result.message || 'Login successful', 'success');
|
|
41
|
+
router.visit(result.data?.redirect || '/dashboard');
|
|
42
|
+
} else {
|
|
43
|
+
const errorMsg = result.errors
|
|
44
|
+
? Object.values(result.errors).flat().join(', ')
|
|
45
|
+
: result.message || 'Login failed';
|
|
46
|
+
Toast(errorMsg, 'error');
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
Toast('An error occurred. Please try again.', 'error');
|
|
50
|
+
} finally {
|
|
51
|
+
loading = false;
|
|
52
|
+
}
|
|
30
53
|
}
|
|
31
54
|
</script>
|
|
32
55
|
|
|
@@ -13,38 +13,57 @@
|
|
|
13
13
|
email: string;
|
|
14
14
|
password: string;
|
|
15
15
|
name: string;
|
|
16
|
-
phone: string;
|
|
17
|
-
password_confirmation: string;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
let form: RegisterForm = {
|
|
21
19
|
email: '',
|
|
22
20
|
password: '',
|
|
23
21
|
name: '',
|
|
24
|
-
phone: '',
|
|
25
|
-
password_confirmation: '',
|
|
26
22
|
}
|
|
27
23
|
|
|
24
|
+
let password_confirmation = $state('');
|
|
25
|
+
let loading = $state(false);
|
|
28
26
|
let { error }: { error?: string } = $props();
|
|
29
27
|
|
|
30
28
|
$effect(() => {
|
|
31
29
|
if (error) Toast(error, 'error');
|
|
32
30
|
});
|
|
33
31
|
|
|
34
|
-
function submitForm(): void {
|
|
35
|
-
if (form.password
|
|
36
|
-
Toast("Password
|
|
32
|
+
async function submitForm(): Promise<void> {
|
|
33
|
+
if (form.password !== password_confirmation) {
|
|
34
|
+
Toast("Password and confirmation must match", "error");
|
|
37
35
|
return;
|
|
38
36
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
|
|
38
|
+
loading = true;
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('/api/auth/register', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ name: form.name, email: form.email, password: form.password })
|
|
44
|
+
});
|
|
45
|
+
const result = await response.json();
|
|
46
|
+
|
|
47
|
+
if (result.success) {
|
|
48
|
+
Toast(result.message || 'Registration successful', 'success');
|
|
49
|
+
router.visit(result.data?.redirect || '/dashboard');
|
|
50
|
+
} else {
|
|
51
|
+
const errorMsg = result.errors
|
|
52
|
+
? Object.values(result.errors).flat().join(', ')
|
|
53
|
+
: result.message || 'Registration failed';
|
|
54
|
+
Toast(errorMsg, 'error');
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
Toast('An error occurred. Please try again.', 'error');
|
|
58
|
+
} finally {
|
|
59
|
+
loading = false;
|
|
60
|
+
}
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
function generatePassword(): void {
|
|
45
|
-
const retVal = password_generator(10);
|
|
63
|
+
function generatePassword(): void {
|
|
64
|
+
const retVal = password_generator(10);
|
|
46
65
|
form.password = retVal
|
|
47
|
-
|
|
66
|
+
password_confirmation = retVal
|
|
48
67
|
}
|
|
49
68
|
</script>
|
|
50
69
|
|
|
@@ -144,7 +163,7 @@
|
|
|
144
163
|
</div>
|
|
145
164
|
<div class="space-y-1">
|
|
146
165
|
<label for="confirm-password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 ml-1">Confirm</label>
|
|
147
|
-
<input bind:value={
|
|
166
|
+
<input bind:value={password_confirmation} type="password" name="confirm-password" id="confirm-password"
|
|
148
167
|
placeholder="••••••••"
|
|
149
168
|
class="w-full px-5 py-3 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl focus:ring-2 focus:ring-info-500/50 focus:border-info-500 outline-none transition-all dark:text-white placeholder:text-slate-400" >
|
|
150
169
|
</div>
|
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
interface ResetPasswordForm {
|
|
8
8
|
password: string;
|
|
9
|
-
|
|
10
|
-
id: string;
|
|
9
|
+
token: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
let {
|
|
12
|
+
let { token, error }: { token: string, error?: string } = $props();
|
|
14
13
|
|
|
15
14
|
$effect(() => {
|
|
16
15
|
if (error) Toast(error, 'error');
|
|
@@ -18,23 +17,47 @@
|
|
|
18
17
|
|
|
19
18
|
let form: ResetPasswordForm = {
|
|
20
19
|
password: '',
|
|
21
|
-
|
|
22
|
-
id
|
|
20
|
+
token
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
|
|
23
|
+
let password_confirmation = $state('');
|
|
24
|
+
let loading = $state(false);
|
|
25
|
+
|
|
26
|
+
function generatePassword(): void {
|
|
27
|
+
const retVal = password_generator(10);
|
|
27
28
|
form.password = retVal
|
|
28
|
-
|
|
29
|
+
password_confirmation = retVal
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function submitForm(): void {
|
|
32
|
-
if (form.password
|
|
33
|
-
Toast("Password
|
|
32
|
+
async function submitForm(): Promise<void> {
|
|
33
|
+
if (form.password !== password_confirmation) {
|
|
34
|
+
Toast("Password and confirmation must match", "error")
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
loading = true;
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('/api/auth/reset-password', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ password: form.password, token: form.token })
|
|
44
|
+
});
|
|
45
|
+
const result = await response.json();
|
|
46
|
+
|
|
47
|
+
if (result.success) {
|
|
48
|
+
Toast(result.message || 'Password reset successful', 'success');
|
|
49
|
+
router.visit('/login');
|
|
50
|
+
} else {
|
|
51
|
+
const errorMsg = result.errors
|
|
52
|
+
? Object.values(result.errors).flat().join(', ')
|
|
53
|
+
: result.message || 'Password reset failed';
|
|
54
|
+
Toast(errorMsg, 'error');
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
Toast('An error occurred. Please try again.', 'error');
|
|
58
|
+
} finally {
|
|
59
|
+
loading = false;
|
|
60
|
+
}
|
|
38
61
|
}
|
|
39
62
|
</script>
|
|
40
63
|
|
|
@@ -67,7 +90,7 @@
|
|
|
67
90
|
|
|
68
91
|
<form class="space-y-4 md:space-y-6" on:submit|preventDefault={submitForm}>
|
|
69
92
|
<div>
|
|
70
|
-
<label for="password" class="block mb-2 text-sm font-medium text-slate-200">Password
|
|
93
|
+
<label for="password" class="block mb-2 text-sm font-medium text-slate-200">New Password</label>
|
|
71
94
|
<input
|
|
72
95
|
bind:value={form.password}
|
|
73
96
|
type="password"
|
|
@@ -80,9 +103,9 @@
|
|
|
80
103
|
<button type="button" on:click={generatePassword} class="text-xs text-slate-400 mt-1">Generate Password</button>
|
|
81
104
|
</div>
|
|
82
105
|
<div>
|
|
83
|
-
<label for="confirm-password" class="block mb-2 text-sm font-medium text-slate-200">
|
|
106
|
+
<label for="confirm-password" class="block mb-2 text-sm font-medium text-slate-200">Confirm Password</label>
|
|
84
107
|
<input
|
|
85
|
-
bind:value={
|
|
108
|
+
bind:value={password_confirmation}
|
|
86
109
|
type="password"
|
|
87
110
|
name="confirm-password"
|
|
88
111
|
id="confirm-password"
|
|
@@ -97,7 +120,7 @@
|
|
|
97
120
|
</button>
|
|
98
121
|
|
|
99
122
|
<p class="text-sm font-light text-slate-400">
|
|
100
|
-
|
|
123
|
+
Remember your password? <a href="/login" use:inertia class="font-medium text-primary-400 hover:underline">Login here</a>
|
|
101
124
|
</p>
|
|
102
125
|
</form>
|
|
103
126
|
</div>
|