nuxt-users 1.6.5 → 1.6.7
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.
- package/CHANGELOG.md +63 -0
- package/dist/cli.mjs +1 -1
- package/dist/module.d.mts +4 -70
- package/dist/module.d.ts +4 -70
- package/dist/module.json +1 -1
- package/dist/module.mjs +28 -27
- package/dist/runtime/components/LoginForm.vue +6 -3
- package/dist/runtime/components/LoginForm.vue.d.ts +4 -4
- package/dist/runtime/components/LogoutLink.vue +21 -7
- package/dist/runtime/components/LogoutLink.vue.d.ts +22 -5
- package/dist/runtime/components/PasswordStrengthIndicator.vue +38 -27
- package/dist/runtime/components/ProfileInfo.vue +33 -0
- package/dist/runtime/components/ResetPasswordForm.vue +89 -96
- package/dist/runtime/components/ResetPasswordForm.vue.d.ts +15 -1
- package/dist/runtime/composables/useAuth.d.ts +2 -2
- package/dist/runtime/composables/useAuth.js +2 -2
- package/dist/runtime/composables/usePasswordValidation.d.ts +1 -1
- package/dist/runtime/middleware/auth.client.js +4 -4
- package/dist/runtime/server/api/{login.post.js → auth/login.post.js} +5 -3
- package/dist/runtime/server/api/{logout.get.js → auth/logout.get.js} +1 -1
- package/dist/runtime/server/api/{update-password.post.js → auth/update-password.post.js} +2 -2
- package/dist/runtime/server/api/user/profile.get.d.ts +4 -0
- package/dist/runtime/server/api/{profile.get.js → user/profile.get.js} +2 -2
- package/dist/runtime/server/middleware/auth.server.js +3 -2
- package/dist/runtime/server/tasks/cleanup-tokens.d.ts +2 -0
- package/dist/runtime/server/tasks/cleanup-tokens.js +30 -0
- package/dist/runtime/server/utils/index.d.ts +1 -1
- package/dist/runtime/server/utils/index.js +1 -1
- package/dist/runtime/server/utils/user.d.ts +20 -0
- package/dist/runtime/server/utils/user.js +45 -1
- package/package.json +1 -1
- package/dist/runtime/components/ForgotPasswordForm.vue +0 -77
- package/dist/runtime/components/ProfileForm.vue +0 -240
- package/dist/runtime/components/ProfileForm.vue.d.ts +0 -20
- package/dist/runtime/server/api/profile.get.d.ts +0 -2
- /package/dist/runtime/components/{ForgotPasswordForm.vue.d.ts → ProfileInfo.vue.d.ts} +0 -0
- /package/dist/runtime/server/api/{login.post.d.ts → auth/login.post.d.ts} +0 -0
- /package/dist/runtime/server/api/{logout.get.d.ts → auth/logout.get.d.ts} +0 -0
- /package/dist/runtime/server/api/{update-password.post.d.ts → auth/update-password.post.d.ts} +0 -0
|
@@ -1,118 +1,98 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref,
|
|
3
|
-
import { useRoute, useRouter } from "#app";
|
|
2
|
+
import { ref, watch } from "vue";
|
|
4
3
|
import { usePasswordValidation } from "../composables/usePasswordValidation";
|
|
5
4
|
import { useRuntimeConfig } from "#imports";
|
|
6
5
|
import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
apiEndpoint: { type: String, required: false, default: "/api/user/profile" },
|
|
8
|
+
updatePasswordEndpoint: { type: String, required: false, default: "/api/auth/update-password" }
|
|
10
9
|
});
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
onMounted(() => {
|
|
20
|
-
if (typeof route.query.token === "string") {
|
|
21
|
-
tokenFromUrl.value = route.query.token;
|
|
22
|
-
}
|
|
23
|
-
if (typeof route.query.email === "string") {
|
|
24
|
-
emailFromUrl.value = decodeURIComponent(route.query.email);
|
|
25
|
-
}
|
|
26
|
-
if (!tokenFromUrl.value || !emailFromUrl.value) {
|
|
27
|
-
message.value = "Invalid or missing password reset token or email in URL. Cannot reset password.";
|
|
28
|
-
isError.value = true;
|
|
29
|
-
formInvalidDueToToken.value = true;
|
|
30
|
-
}
|
|
10
|
+
const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
|
|
11
|
+
const isPasswordLoading = ref(false);
|
|
12
|
+
const passwordError = ref("");
|
|
13
|
+
const passwordSuccess = ref("");
|
|
14
|
+
const passwordForm = ref({
|
|
15
|
+
currentPassword: "",
|
|
16
|
+
newPassword: "",
|
|
17
|
+
newPasswordConfirmation: ""
|
|
31
18
|
});
|
|
32
|
-
const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
|
|
33
19
|
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
34
20
|
const moduleOptions = nuxtUsers;
|
|
35
21
|
const passwordValidation = usePasswordValidation(moduleOptions);
|
|
36
|
-
watch(() =>
|
|
22
|
+
watch(() => passwordForm.value.newPassword, (newPassword) => {
|
|
37
23
|
if (newPassword) {
|
|
38
24
|
passwordValidation.validate(newPassword);
|
|
39
25
|
} else {
|
|
40
26
|
passwordValidation.clearValidation();
|
|
41
27
|
}
|
|
42
28
|
});
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (formData.password !== formData.password_confirm) {
|
|
50
|
-
message.value = "Passwords do not match.";
|
|
51
|
-
isError.value = true;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
loading.value = true;
|
|
55
|
-
message.value = "";
|
|
56
|
-
isError.value = false;
|
|
29
|
+
const updatePassword = async () => {
|
|
30
|
+
isPasswordLoading.value = true;
|
|
31
|
+
passwordError.value = "";
|
|
32
|
+
passwordSuccess.value = "";
|
|
57
33
|
try {
|
|
58
|
-
|
|
34
|
+
await $fetch(props.updatePasswordEndpoint, {
|
|
59
35
|
method: "POST",
|
|
60
|
-
|
|
61
|
-
"Content-Type": "application/json",
|
|
62
|
-
"Accept": "application/json"
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
-
token: tokenFromUrl.value,
|
|
66
|
-
email: emailFromUrl.value,
|
|
67
|
-
password: formData.password,
|
|
68
|
-
password_confirmation: formData.password_confirm
|
|
69
|
-
})
|
|
36
|
+
body: passwordForm.value
|
|
70
37
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
formData.password_confirm = "";
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
router.push("/login");
|
|
81
|
-
}, 3e3);
|
|
38
|
+
passwordSuccess.value = "Password updated successfully";
|
|
39
|
+
emit("password-updated");
|
|
40
|
+
passwordForm.value = {
|
|
41
|
+
currentPassword: "",
|
|
42
|
+
newPassword: "",
|
|
43
|
+
newPasswordConfirmation: ""
|
|
44
|
+
};
|
|
82
45
|
} catch (err) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
message.value = "Failed to reset password.";
|
|
87
|
-
}
|
|
88
|
-
console.error("[Nuxt Users] Reset Password Form Error:", err);
|
|
89
|
-
isError.value = true;
|
|
46
|
+
const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Failed to update password";
|
|
47
|
+
passwordError.value = errorMessage;
|
|
48
|
+
emit("password-error", errorMessage);
|
|
90
49
|
} finally {
|
|
91
|
-
|
|
50
|
+
isPasswordLoading.value = false;
|
|
92
51
|
}
|
|
93
52
|
};
|
|
94
53
|
</script>
|
|
95
54
|
|
|
96
55
|
<template>
|
|
97
56
|
<div>
|
|
98
|
-
<h2>
|
|
99
|
-
|
|
57
|
+
<h2>Change Password</h2>
|
|
58
|
+
|
|
59
|
+
<form @submit.prevent="updatePassword">
|
|
60
|
+
<div class="form-group">
|
|
61
|
+
<label
|
|
62
|
+
for="currentPassword"
|
|
63
|
+
class="form-label"
|
|
64
|
+
>Current Password</label>
|
|
65
|
+
<input
|
|
66
|
+
id="currentPassword"
|
|
67
|
+
v-model="passwordForm.currentPassword"
|
|
68
|
+
type="password"
|
|
69
|
+
class="form-input"
|
|
70
|
+
required
|
|
71
|
+
:disabled="isPasswordLoading"
|
|
72
|
+
>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
100
75
|
<div class="form-group">
|
|
101
|
-
<label
|
|
76
|
+
<label
|
|
77
|
+
for="newPassword"
|
|
78
|
+
class="form-label"
|
|
79
|
+
>New Password</label>
|
|
102
80
|
<input
|
|
103
|
-
id="
|
|
104
|
-
v-model="
|
|
81
|
+
id="newPassword"
|
|
82
|
+
v-model="passwordForm.newPassword"
|
|
105
83
|
type="password"
|
|
106
|
-
|
|
107
|
-
placeholder="Enter new password"
|
|
84
|
+
class="form-input"
|
|
108
85
|
required
|
|
109
86
|
:minlength="moduleOptions.passwordValidation?.minLength || 8"
|
|
110
|
-
:class="{ error: passwordValidation.errors.value.length > 0 &&
|
|
87
|
+
:class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
|
|
88
|
+
:disabled="isPasswordLoading"
|
|
111
89
|
>
|
|
90
|
+
|
|
112
91
|
<PasswordStrengthIndicator
|
|
113
|
-
:password="
|
|
92
|
+
:password="passwordForm.newPassword"
|
|
114
93
|
:validation-result="passwordValidation.validationResult.value"
|
|
115
94
|
/>
|
|
95
|
+
|
|
116
96
|
<!-- Password requirements -->
|
|
117
97
|
<small class="form-help">
|
|
118
98
|
Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
|
|
@@ -122,35 +102,48 @@ const handleResetPassword = async () => {
|
|
|
122
102
|
<span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
|
|
123
103
|
</small>
|
|
124
104
|
</div>
|
|
105
|
+
|
|
125
106
|
<div class="form-group">
|
|
126
|
-
<label
|
|
107
|
+
<label
|
|
108
|
+
for="newPasswordConfirmation"
|
|
109
|
+
class="form-label"
|
|
110
|
+
>Confirm New Password</label>
|
|
127
111
|
<input
|
|
128
|
-
id="
|
|
129
|
-
v-model="
|
|
112
|
+
id="newPasswordConfirmation"
|
|
113
|
+
v-model="passwordForm.newPasswordConfirmation"
|
|
130
114
|
type="password"
|
|
131
|
-
|
|
132
|
-
placeholder="Confirm new password"
|
|
115
|
+
class="form-input"
|
|
133
116
|
required
|
|
134
|
-
:
|
|
117
|
+
:disabled="isPasswordLoading"
|
|
135
118
|
>
|
|
136
119
|
</div>
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
v-if="passwordError"
|
|
123
|
+
class="error-message"
|
|
124
|
+
>
|
|
125
|
+
{{ passwordError }}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div
|
|
129
|
+
v-if="passwordSuccess"
|
|
130
|
+
class="success-message"
|
|
131
|
+
>
|
|
132
|
+
{{ passwordSuccess }}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
137
135
|
<button
|
|
138
136
|
type="submit"
|
|
139
|
-
|
|
137
|
+
class="btn btn-primary"
|
|
138
|
+
:disabled="isPasswordLoading"
|
|
140
139
|
>
|
|
141
|
-
|
|
140
|
+
<span v-if="isPasswordLoading">Updating...</span>
|
|
141
|
+
<span v-else>Update Password</span>
|
|
142
142
|
</button>
|
|
143
|
-
<p
|
|
144
|
-
v-if="message"
|
|
145
|
-
:class="{ error: isError, success: !isError }"
|
|
146
|
-
class="form-message"
|
|
147
|
-
>
|
|
148
|
-
{{ message }}
|
|
149
|
-
</p>
|
|
150
143
|
</form>
|
|
151
144
|
</div>
|
|
152
145
|
</template>
|
|
153
146
|
|
|
154
147
|
<style scoped>
|
|
155
|
-
|
|
148
|
+
div{background-color:var(--color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em;margin:0 auto;max-width:42em;padding:1em}h2{border-bottom:.1em solid var(--color-border)}form{gap:1em}.form-group,form{display:flex;flex-direction:column}.form-group{gap:.5em;width:90%}.form-label{color:var(--color-text-primary);font-size:1rem;font-weight:500}.form-input{border:1px solid var(--color-border);border-radius:.5em;font-size:1rem;padding:.5em 1em;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 .1em rgba(var(--color-primary-rgb),.1);outline:none}.form-input:disabled{background-color:var(--color-bg-secondary);cursor:not-allowed}.form-help{color:var(--color-text-secondary);font-size:.75rem;margin-top:.25em}.btn{align-self:flex-start;border:none;border-radius:.5em;cursor:pointer;font-size:1rem;font-weight:500;padding:.5em 1em;transition:background-color .2s,transform .1s}.btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.btn-primary{background-color:var(--color-primary);color:#fff}.btn-primary:hover:not(:disabled){background-color:var(--color-primary-dark);transform:translateY(-1px)}.error-message{background-color:var(--color-error-bg);border:1px solid var(--color-error-border);border-radius:.5em;color:var(--color-error);font-size:.875rem;padding:12px}.success-message{background-color:#f0fff4;border:1px solid #9ae6b4;border-radius:6px;color:#38a169;font-size:.875rem;padding:12px}
|
|
156
149
|
</style>
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import type { UserWithoutPassword, ResetPasswordFormProps } from '../../types.js';
|
|
2
|
+
declare const _default: import("vue").DefineComponent<ResetPasswordFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
3
|
+
success: (user: UserWithoutPassword) => any;
|
|
4
|
+
error: (error: string) => any;
|
|
5
|
+
"password-error": (error: string) => any;
|
|
6
|
+
"password-updated": () => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<ResetPasswordFormProps> & Readonly<{
|
|
8
|
+
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
9
|
+
onError?: ((error: string) => any) | undefined;
|
|
10
|
+
"onPassword-error"?: ((error: string) => any) | undefined;
|
|
11
|
+
"onPassword-updated"?: (() => any) | undefined;
|
|
12
|
+
}>, {
|
|
13
|
+
apiEndpoint: string;
|
|
14
|
+
updatePasswordEndpoint: string;
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
2
16
|
export default _default;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { User, UserWithoutPassword } from '../../types.js';
|
|
2
2
|
export declare const useAuth: () => {
|
|
3
3
|
user: Readonly<import("vue").Ref<{
|
|
4
|
-
readonly email: string;
|
|
5
4
|
readonly id: number;
|
|
5
|
+
readonly email: string;
|
|
6
6
|
readonly name: string;
|
|
7
7
|
readonly role: string;
|
|
8
8
|
readonly created_at: string;
|
|
9
9
|
readonly updated_at: string;
|
|
10
10
|
} | null, {
|
|
11
|
-
readonly email: string;
|
|
12
11
|
readonly id: number;
|
|
12
|
+
readonly email: string;
|
|
13
13
|
readonly name: string;
|
|
14
14
|
readonly role: string;
|
|
15
15
|
readonly created_at: string;
|
|
@@ -12,7 +12,7 @@ export const useAuth = () => {
|
|
|
12
12
|
};
|
|
13
13
|
const logout = async () => {
|
|
14
14
|
try {
|
|
15
|
-
await $fetch("/api/logout", {
|
|
15
|
+
await $fetch("/api/auth/logout", {
|
|
16
16
|
method: "GET"
|
|
17
17
|
});
|
|
18
18
|
user.value = null;
|
|
@@ -26,7 +26,7 @@ export const useAuth = () => {
|
|
|
26
26
|
};
|
|
27
27
|
const fetchUser = async () => {
|
|
28
28
|
try {
|
|
29
|
-
const response = await $fetch("/api/profile", {
|
|
29
|
+
const response = await $fetch("/api/user/profile", {
|
|
30
30
|
method: "GET"
|
|
31
31
|
});
|
|
32
32
|
user.value = response.user;
|
|
@@ -25,7 +25,7 @@ export declare const usePasswordValidation: (moduleOptions?: ModuleOptions, opti
|
|
|
25
25
|
isValid: import("vue").ComputedRef<boolean>;
|
|
26
26
|
errors: import("vue").ComputedRef<string[]>;
|
|
27
27
|
hints: import("vue").ComputedRef<string[]>;
|
|
28
|
-
strength: import("vue").ComputedRef<"
|
|
28
|
+
strength: import("vue").ComputedRef<"weak" | "medium" | "strong">;
|
|
29
29
|
score: import("vue").ComputedRef<number>;
|
|
30
30
|
strengthColor: import("vue").ComputedRef<string>;
|
|
31
31
|
strengthText: import("vue").ComputedRef<string>;
|
|
@@ -11,10 +11,10 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
11
11
|
console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
const {
|
|
15
|
-
if (!
|
|
16
|
-
console.log(
|
|
14
|
+
const { isAuthenticated } = useAuth();
|
|
15
|
+
if (!isAuthenticated.value) {
|
|
16
|
+
console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
|
|
17
17
|
return navigateTo("/login");
|
|
18
18
|
}
|
|
19
|
-
console.log("[Nuxt Users] client.middleware.auth.global", {
|
|
19
|
+
console.log("[Nuxt Users] client.middleware.auth.global", { isAuthenticated: isAuthenticated.value, to: to.path });
|
|
20
20
|
});
|
|
@@ -2,7 +2,7 @@ import { createError, defineEventHandler, readBody, setCookie } from "h3";
|
|
|
2
2
|
import bcrypt from "bcrypt";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { useRuntimeConfig } from "#imports";
|
|
5
|
-
import { useDb } from "
|
|
5
|
+
import { useDb } from "../../utils/index.js";
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
7
|
const body = await readBody(event);
|
|
8
8
|
const { email, password } = body;
|
|
@@ -35,9 +35,11 @@ export default defineEventHandler(async (event) => {
|
|
|
35
35
|
}
|
|
36
36
|
const token = crypto.randomBytes(64).toString("hex");
|
|
37
37
|
const tokenName = "auth_token";
|
|
38
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
39
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + options.auth.tokenExpiration);
|
|
38
40
|
await db.sql`
|
|
39
|
-
INSERT INTO {${personalAccessTokensTable}} (tokenable_type, tokenable_id, name, token, created_at, updated_at)
|
|
40
|
-
VALUES ('user', ${user.id}, ${tokenName}, ${token}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
41
|
+
INSERT INTO {${personalAccessTokensTable}} (tokenable_type, tokenable_id, name, token, expires_at, created_at, updated_at)
|
|
42
|
+
VALUES ('user', ${user.id}, ${tokenName}, ${token}, ${expiresAt.toISOString().slice(0, 19).replace("T", " ")}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
41
43
|
`;
|
|
42
44
|
setCookie(event, "auth_token", token, {
|
|
43
45
|
httpOnly: true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineEventHandler, getCookie, setCookie } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
-
import { useDb } from "
|
|
3
|
+
import { useDb } from "../../utils/index.js";
|
|
4
4
|
export default defineEventHandler(async (event) => {
|
|
5
5
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
6
|
const options = nuxtUsers;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getCookie, readBody } from "h3";
|
|
2
2
|
import bcrypt from "bcrypt";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
|
-
import { getCurrentUserFromToken, updateUserPassword } from "
|
|
5
|
-
import { validatePassword, getPasswordValidationOptions } from "
|
|
4
|
+
import { getCurrentUserFromToken, updateUserPassword } from "../../utils/index.js";
|
|
5
|
+
import { validatePassword, getPasswordValidationOptions } from "../../../../utils";
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
8
8
|
const options = nuxtUsers;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getCookie } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
-
import { getCurrentUserFromToken } from "
|
|
3
|
+
import { getCurrentUserFromToken } from "../../utils/index.js";
|
|
4
4
|
export default defineEventHandler(async (event) => {
|
|
5
5
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
6
|
const options = nuxtUsers;
|
|
@@ -18,5 +18,5 @@ export default defineEventHandler(async (event) => {
|
|
|
18
18
|
statusMessage: "Unauthorized - Invalid authentication token"
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
-
return user;
|
|
21
|
+
return { user };
|
|
22
22
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineEventHandler, getCookie, sendRedirect } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
import { getCurrentUserFromToken } from "../utils/index.js";
|
|
4
|
+
const noAuthPaths = ["/login", "/api/auth/login", "/api/auth/forgot-password"];
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
6
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
7
|
const options = nuxtUsers;
|
|
@@ -8,7 +9,7 @@ export default defineEventHandler(async (event) => {
|
|
|
8
9
|
if (!isPageOrApiRoute) {
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
|
-
if (
|
|
12
|
+
if (noAuthPaths.includes(event.path)) {
|
|
12
13
|
console.log("[Nuxt Users] server.middleware.auth.global: /login");
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
@@ -26,6 +27,6 @@ export default defineEventHandler(async (event) => {
|
|
|
26
27
|
console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} No user found redirecting to login`);
|
|
27
28
|
return sendRedirect(event, "/login");
|
|
28
29
|
}
|
|
29
|
-
console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request for ${user.id}`);
|
|
30
|
+
console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request to ${event.path} for ${user.id}`);
|
|
30
31
|
return;
|
|
31
32
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineTask } from "nitropack/runtime/task";
|
|
2
|
+
export default defineTask({
|
|
3
|
+
meta: {
|
|
4
|
+
name: "nuxt-users:cleanup-tokens",
|
|
5
|
+
description: "Clean up expired personal access tokens and tokens without expiration"
|
|
6
|
+
},
|
|
7
|
+
async run({ payload }) {
|
|
8
|
+
const { useRuntimeConfig } = await import("#imports");
|
|
9
|
+
const { cleanupPersonalAccessTokens } = await import("../utils/index.js");
|
|
10
|
+
const { nuxtUsers } = useRuntimeConfig();
|
|
11
|
+
const options = nuxtUsers;
|
|
12
|
+
if (!options) {
|
|
13
|
+
throw new Error("Nuxt Users module not configured");
|
|
14
|
+
}
|
|
15
|
+
const includeNoExpiration = payload?.includeNoExpiration ?? true;
|
|
16
|
+
try {
|
|
17
|
+
const result = await cleanupPersonalAccessTokens(options, includeNoExpiration);
|
|
18
|
+
return {
|
|
19
|
+
result: "success",
|
|
20
|
+
expiredTokensRemoved: result.expiredCount,
|
|
21
|
+
noExpirationTokensRemoved: result.noExpirationCount,
|
|
22
|
+
totalTokensCleaned: result.totalCount,
|
|
23
|
+
includeNoExpiration
|
|
24
|
+
};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("[Nuxt Users] Token cleanup failed:", error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useDb, checkTableExists } from './db.js';
|
|
2
|
-
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers } from './user.js';
|
|
2
|
+
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers, deleteExpiredPersonalAccessTokens, deleteTokensWithoutExpiration, cleanupPersonalAccessTokens, revokeUserTokens } from './user.js';
|
|
3
3
|
export { createUsersTable } from './create-users-table.js';
|
|
4
4
|
export { createPersonalAccessTokensTable } from './create-personal-access-tokens-table.js';
|
|
5
5
|
export { createPasswordResetTokensTable } from './create-password-reset-tokens-table.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useDb, checkTableExists } from "./db.js";
|
|
2
|
-
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers } from "./user.js";
|
|
2
|
+
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers, deleteExpiredPersonalAccessTokens, deleteTokensWithoutExpiration, cleanupPersonalAccessTokens, revokeUserTokens } from "./user.js";
|
|
3
3
|
export { createUsersTable } from "./create-users-table.js";
|
|
4
4
|
export { createPersonalAccessTokensTable } from "./create-personal-access-tokens-table.js";
|
|
5
5
|
export { createPasswordResetTokensTable } from "./create-password-reset-tokens-table.js";
|
|
@@ -24,4 +24,24 @@ export declare const hasAnyUsers: (options: ModuleOptions) => Promise<boolean>;
|
|
|
24
24
|
* Gets the current user from an authentication token.
|
|
25
25
|
*/
|
|
26
26
|
export declare const getCurrentUserFromToken: <T extends boolean = false>(token: string, options: ModuleOptions, withPass?: T) => Promise<T extends true ? User | null : UserWithoutPassword | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Deletes expired personal access tokens from the database.
|
|
29
|
+
*/
|
|
30
|
+
export declare const deleteExpiredPersonalAccessTokens: (options: ModuleOptions) => Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* Deletes tokens without expiration dates (legacy tokens or security cleanup).
|
|
33
|
+
*/
|
|
34
|
+
export declare const deleteTokensWithoutExpiration: (options: ModuleOptions) => Promise<number>;
|
|
35
|
+
/**
|
|
36
|
+
* Comprehensive token cleanup: removes expired tokens and optionally tokens without expiration.
|
|
37
|
+
*/
|
|
38
|
+
export declare const cleanupPersonalAccessTokens: (options: ModuleOptions, includeNoExpiration?: boolean) => Promise<{
|
|
39
|
+
expiredCount: number;
|
|
40
|
+
noExpirationCount: number;
|
|
41
|
+
totalCount: number;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Revokes all tokens for a specific user.
|
|
45
|
+
*/
|
|
46
|
+
export declare const revokeUserTokens: (userId: number, options: ModuleOptions) => Promise<void>;
|
|
27
47
|
export {};
|
|
@@ -76,13 +76,19 @@ export const getCurrentUserFromToken = async (token, options, withPass = false)
|
|
|
76
76
|
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
77
77
|
const usersTable = options.tables.users;
|
|
78
78
|
const tokenResult = await db.sql`
|
|
79
|
-
SELECT tokenable_id FROM {${personalAccessTokensTable}}
|
|
79
|
+
SELECT tokenable_id, expires_at FROM {${personalAccessTokensTable}}
|
|
80
80
|
WHERE token = ${token}
|
|
81
|
+
AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
|
81
82
|
`;
|
|
82
83
|
if (tokenResult.rows.length === 0) {
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
86
|
const userId = tokenResult.rows[0].tokenable_id;
|
|
87
|
+
await db.sql`
|
|
88
|
+
UPDATE {${personalAccessTokensTable}}
|
|
89
|
+
SET last_used_at = CURRENT_TIMESTAMP
|
|
90
|
+
WHERE token = ${token}
|
|
91
|
+
`;
|
|
86
92
|
const userResult = await db.sql`
|
|
87
93
|
SELECT * FROM {${usersTable}}
|
|
88
94
|
WHERE id = ${userId}
|
|
@@ -97,3 +103,41 @@ export const getCurrentUserFromToken = async (token, options, withPass = false)
|
|
|
97
103
|
const { password: _, ...userWithoutPassword } = user;
|
|
98
104
|
return userWithoutPassword;
|
|
99
105
|
};
|
|
106
|
+
export const deleteExpiredPersonalAccessTokens = async (options) => {
|
|
107
|
+
const db = await useDb(options);
|
|
108
|
+
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
109
|
+
const result = await db.sql`
|
|
110
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
111
|
+
WHERE expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP
|
|
112
|
+
`;
|
|
113
|
+
const deletedCount = result.rows?.length || 0;
|
|
114
|
+
console.log(`[Nuxt Users] ${deletedCount} expired personal access tokens deleted.`);
|
|
115
|
+
return deletedCount;
|
|
116
|
+
};
|
|
117
|
+
export const deleteTokensWithoutExpiration = async (options) => {
|
|
118
|
+
const db = await useDb(options);
|
|
119
|
+
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
120
|
+
const result = await db.sql`
|
|
121
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
122
|
+
WHERE expires_at IS NULL
|
|
123
|
+
`;
|
|
124
|
+
const deletedCount = result.rows?.length || 0;
|
|
125
|
+
console.log(`[Nuxt Users] ${deletedCount} tokens without expiration deleted.`);
|
|
126
|
+
return deletedCount;
|
|
127
|
+
};
|
|
128
|
+
export const cleanupPersonalAccessTokens = async (options, includeNoExpiration = true) => {
|
|
129
|
+
const expiredCount = await deleteExpiredPersonalAccessTokens(options);
|
|
130
|
+
const noExpirationCount = includeNoExpiration ? await deleteTokensWithoutExpiration(options) : 0;
|
|
131
|
+
const totalCount = expiredCount + noExpirationCount;
|
|
132
|
+
console.log(`[Nuxt Users] Token cleanup completed: ${expiredCount} expired + ${noExpirationCount} without expiration = ${totalCount} total tokens removed.`);
|
|
133
|
+
return { expiredCount, noExpirationCount, totalCount };
|
|
134
|
+
};
|
|
135
|
+
export const revokeUserTokens = async (userId, options) => {
|
|
136
|
+
const db = await useDb(options);
|
|
137
|
+
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
138
|
+
await db.sql`
|
|
139
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
140
|
+
WHERE tokenable_type = 'user' AND tokenable_id = ${userId}
|
|
141
|
+
`;
|
|
142
|
+
console.log(`[Nuxt Users] All tokens revoked for user ID: ${userId}`);
|
|
143
|
+
};
|
package/package.json
CHANGED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, reactive } from "vue";
|
|
3
|
-
const formData = reactive({
|
|
4
|
-
email: ""
|
|
5
|
-
});
|
|
6
|
-
const message = ref("");
|
|
7
|
-
const loading = ref(false);
|
|
8
|
-
const isError = ref(false);
|
|
9
|
-
const handleForgotPassword = async () => {
|
|
10
|
-
loading.value = true;
|
|
11
|
-
message.value = "";
|
|
12
|
-
isError.value = false;
|
|
13
|
-
try {
|
|
14
|
-
const response = await fetch("/api/forgot-password", {
|
|
15
|
-
method: "POST",
|
|
16
|
-
headers: {
|
|
17
|
-
"Content-Type": "application/json",
|
|
18
|
-
"Accept": "application/json"
|
|
19
|
-
},
|
|
20
|
-
body: JSON.stringify({ email: formData.email })
|
|
21
|
-
});
|
|
22
|
-
const responseData = await response.json();
|
|
23
|
-
if (!response.ok) {
|
|
24
|
-
throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
|
|
25
|
-
}
|
|
26
|
-
message.value = responseData.message;
|
|
27
|
-
isError.value = false;
|
|
28
|
-
formData.email = "";
|
|
29
|
-
} catch (err) {
|
|
30
|
-
if (err instanceof Error) {
|
|
31
|
-
message.value = err.message;
|
|
32
|
-
} else {
|
|
33
|
-
message.value = "Failed to send password reset link.";
|
|
34
|
-
}
|
|
35
|
-
console.error("[Nuxt Users] Forgot Password Form Error:", err);
|
|
36
|
-
isError.value = true;
|
|
37
|
-
} finally {
|
|
38
|
-
loading.value = false;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<template>
|
|
44
|
-
<div>
|
|
45
|
-
<h2>Forgot Password</h2>
|
|
46
|
-
<form @submit.prevent="handleForgotPassword">
|
|
47
|
-
<div class="form-group">
|
|
48
|
-
<label for="email">Email</label>
|
|
49
|
-
<input
|
|
50
|
-
id="email"
|
|
51
|
-
v-model="formData.email"
|
|
52
|
-
type="email"
|
|
53
|
-
name="email"
|
|
54
|
-
placeholder="your@email.com"
|
|
55
|
-
required
|
|
56
|
-
>
|
|
57
|
-
</div>
|
|
58
|
-
<button
|
|
59
|
-
type="submit"
|
|
60
|
-
:disabled="loading"
|
|
61
|
-
>
|
|
62
|
-
{{ loading ? "Sending..." : "Send Password Reset Link" }}
|
|
63
|
-
</button>
|
|
64
|
-
<p
|
|
65
|
-
v-if="message"
|
|
66
|
-
:class="{ error: isError, success: !isError }"
|
|
67
|
-
class="form-message"
|
|
68
|
-
>
|
|
69
|
-
{{ message }}
|
|
70
|
-
</p>
|
|
71
|
-
</form>
|
|
72
|
-
</div>
|
|
73
|
-
</template>
|
|
74
|
-
|
|
75
|
-
<style scoped>
|
|
76
|
-
.error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1em}.form-group{margin-bottom:1em}label{display:block;font-weight:700;margin-bottom:.25em}input[type=email]{border:1px solid var(--color-border-dark);border-radius:4px;box-sizing:border-box;padding:.75em;width:100%}button[type=submit]{background-color:var(--color-gray-800);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75em 1.5em}button[type=submit]:disabled{background-color:var(--color-gray-500);cursor:not-allowed}
|
|
77
|
-
</style>
|