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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
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: JWT_EXPIRES_IN }
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: JWT_EXPIRES_IN }
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', '', { ...COOKIE_OPTIONS, maxAge: 0 });
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', '', { maxAge: 0 });
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', '', { maxAge: 0 });
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 axios from "axios";
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
- const result = await api(() => axios.post("/forgot-password", form));
27
-
28
- if (result.success) {
29
- success = true;
30
- form.email = "";
31
- form.phone = "";
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
- Link reset password telah dikirim ke email atau nomor telepon Anda.
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 atau Nomor Telepon</label>
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="text"
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 atau 08xxxxxxxxxx"
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
- Kirim Link Reset Password
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
- Ingat password Anda? <a href="/login" use:inertia class="font-medium text-primary-400 hover:underline">Login disini</a>
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
- router.post("/login", { email: form.email, password: form.password })
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 != form.password_confirmation) {
36
- Toast("Password dan konfirmasi password harus sama", "error");
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
- form.phone = form.phone.toString()
41
- router.post("/register", form as any)
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
- form.password_confirmation = retVal
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={form.password_confirmation} type="password" name="confirm-password" id="confirm-password"
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
- password_confirmation: string;
10
- id: string;
9
+ token: string;
11
10
  }
12
11
 
13
- let { id, error }: { id: string, error?: string } = $props();
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
- password_confirmation: '',
22
- id
20
+ token
23
21
  }
24
-
25
- function generatePassword(): void {
26
- const retVal = password_generator(10);
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
- form.password_confirmation = retVal
29
+ password_confirmation = retVal
29
30
  }
30
31
 
31
- function submitForm(): void {
32
- if (form.password != form.password_confirmation) {
33
- Toast("Password dan konfirmasi password harus sama", "error")
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
- router.post(`/reset-password`, form as any)
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 Baru</label>
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">Konfirmasi Password</label>
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={form.password_confirmation}
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
- Ingat password Anda? <a href="/login" use:inertia class="font-medium text-primary-400 hover:underline">Login disini</a>
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>