or3-provider-basic-auth 0.0.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.
- package/README.md +99 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +42 -0
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.d.vue.ts +12 -0
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue +133 -0
- package/dist/runtime/components/BasicAuthChangePasswordModal.client.vue.d.ts +12 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.d.vue.ts +11 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue +164 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue.d.ts +11 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.d.vue.ts +13 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.vue +136 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.vue.d.ts +13 -0
- package/dist/runtime/components/BasicAuthUserMenu.client.d.vue.ts +12 -0
- package/dist/runtime/components/BasicAuthUserMenu.client.vue +106 -0
- package/dist/runtime/components/BasicAuthUserMenu.client.vue.d.ts +12 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.d.vue.ts +2 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.vue +154 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.vue.d.ts +2 -0
- package/dist/runtime/lib/constants.d.ts +7 -0
- package/dist/runtime/lib/constants.js +7 -0
- package/dist/runtime/plugins/auth-status.client.d.ts +2 -0
- package/dist/runtime/plugins/auth-status.client.js +38 -0
- package/dist/runtime/plugins/basic-auth-ui.client.d.ts +2 -0
- package/dist/runtime/plugins/basic-auth-ui.client.js +46 -0
- package/dist/runtime/server/admin/adapters/auth-basic-auth.d.ts +2 -0
- package/dist/runtime/server/admin/adapters/auth-basic-auth.js +34 -0
- package/dist/runtime/server/api/basic-auth/_helpers.d.ts +6 -0
- package/dist/runtime/server/api/basic-auth/_helpers.js +26 -0
- package/dist/runtime/server/api/basic-auth/change-password.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/change-password.post.js +49 -0
- package/dist/runtime/server/api/basic-auth/refresh.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/refresh.post.js +78 -0
- package/dist/runtime/server/api/basic-auth/register.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/register.post.js +112 -0
- package/dist/runtime/server/api/basic-auth/sign-in.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/sign-in.post.js +75 -0
- package/dist/runtime/server/api/basic-auth/sign-out.post.d.ts +8 -0
- package/dist/runtime/server/api/basic-auth/sign-out.post.js +37 -0
- package/dist/runtime/server/auth/basic-auth-provider.d.ts +2 -0
- package/dist/runtime/server/auth/basic-auth-provider.js +41 -0
- package/dist/runtime/server/auth/index.d.ts +1 -0
- package/dist/runtime/server/auth/index.js +1 -0
- package/dist/runtime/server/db/client.d.ts +3 -0
- package/dist/runtime/server/db/client.js +106 -0
- package/dist/runtime/server/db/schema.d.ts +21 -0
- package/dist/runtime/server/db/schema.js +0 -0
- package/dist/runtime/server/lib/config.d.ts +22 -0
- package/dist/runtime/server/lib/config.js +94 -0
- package/dist/runtime/server/lib/cookies.d.ts +4 -0
- package/dist/runtime/server/lib/cookies.js +42 -0
- package/dist/runtime/server/lib/errors.d.ts +4 -0
- package/dist/runtime/server/lib/errors.js +25 -0
- package/dist/runtime/server/lib/jwt.d.ts +37 -0
- package/dist/runtime/server/lib/jwt.js +77 -0
- package/dist/runtime/server/lib/password.d.ts +2 -0
- package/dist/runtime/server/lib/password.js +8 -0
- package/dist/runtime/server/lib/rate-limit.d.ts +6 -0
- package/dist/runtime/server/lib/rate-limit.js +163 -0
- package/dist/runtime/server/lib/request-identity.d.ts +16 -0
- package/dist/runtime/server/lib/request-identity.js +32 -0
- package/dist/runtime/server/lib/request-security.d.ts +2 -0
- package/dist/runtime/server/lib/request-security.js +37 -0
- package/dist/runtime/server/lib/session-store.d.ts +46 -0
- package/dist/runtime/server/lib/session-store.js +190 -0
- package/dist/runtime/server/plugins/register.d.ts +2 -0
- package/dist/runtime/server/plugins/register.js +63 -0
- package/dist/runtime/server/token-broker/basic-auth-token-broker.d.ts +6 -0
- package/dist/runtime/server/token-broker/basic-auth-token-broker.js +8 -0
- package/dist/runtime/server/token-broker/index.d.ts +1 -0
- package/dist/runtime/server/token-broker/index.js +1 -0
- package/dist/types.d.mts +7 -0
- package/package.json +70 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UPopover
|
|
3
|
+
v-model:open="isOpen"
|
|
4
|
+
portal
|
|
5
|
+
:content="{ side: 'right', align: 'end', sideOffset: 10 }"
|
|
6
|
+
:ui="{ content: 'z-[260] max-w-[240px] p-0 border-none bg-transparent shadow-none' }"
|
|
7
|
+
>
|
|
8
|
+
<UButton
|
|
9
|
+
v-bind="triggerButtonProps"
|
|
10
|
+
type="button"
|
|
11
|
+
aria-label="Account menu"
|
|
12
|
+
>
|
|
13
|
+
<template #default>
|
|
14
|
+
<span class="flex flex-col items-center gap-1 w-full">
|
|
15
|
+
<UIcon name="i-lucide-user-check" class="h-[18px] w-[18px]" />
|
|
16
|
+
<span class="text-[7px] uppercase tracking-wider whitespace-nowrap">
|
|
17
|
+
Account
|
|
18
|
+
</span>
|
|
19
|
+
</span>
|
|
20
|
+
</template>
|
|
21
|
+
</UButton>
|
|
22
|
+
|
|
23
|
+
<template #content>
|
|
24
|
+
<div
|
|
25
|
+
class="w-[220px] rounded-[var(--md-border-radius)] border-[length:var(--md-border-width)] border-[color:var(--md-border-color)] bg-[var(--md-surface)] overflow-hidden theme-shadow"
|
|
26
|
+
>
|
|
27
|
+
<!-- User identity section -->
|
|
28
|
+
<div class="px-4 pt-4 pb-3">
|
|
29
|
+
<div class="flex items-center gap-3">
|
|
30
|
+
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-[var(--md-success)]/15 text-[var(--md-success)] shrink-0">
|
|
31
|
+
<UIcon name="i-lucide-user" class="w-4 h-4" />
|
|
32
|
+
</div>
|
|
33
|
+
<div class="min-w-0">
|
|
34
|
+
<p class="text-[10px] uppercase tracking-wider text-[var(--md-on-surface)]/50 mb-0.5">
|
|
35
|
+
Signed in as
|
|
36
|
+
</p>
|
|
37
|
+
<p class="text-xs font-medium text-[var(--md-on-surface)] break-all leading-tight">
|
|
38
|
+
{{ email }}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="h-[var(--md-border-width)] bg-[var(--md-border-color)] mx-3" />
|
|
45
|
+
|
|
46
|
+
<!-- Actions -->
|
|
47
|
+
<div class="p-2 space-y-1">
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--md-on-surface)] rounded-[var(--md-border-radius)] hover:bg-[var(--md-surface-hover)] active:bg-[var(--md-surface-active)] transition-colors text-left"
|
|
51
|
+
@click="onChangePassword"
|
|
52
|
+
>
|
|
53
|
+
<UIcon name="i-lucide-key-round" class="w-4 h-4 text-[var(--md-on-surface)]/60 shrink-0" />
|
|
54
|
+
Change Password
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
:disabled="pending"
|
|
60
|
+
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm rounded-[var(--md-border-radius)] text-[var(--md-error)] hover:bg-[var(--md-error)]/10 active:bg-[var(--md-error)]/18 transition-colors text-left disabled:opacity-50"
|
|
61
|
+
@click="onSignOut"
|
|
62
|
+
>
|
|
63
|
+
<UIcon name="i-lucide-log-out" class="w-4 h-4 shrink-0" />
|
|
64
|
+
{{ pending ? "Signing out\u2026" : "Sign Out" }}
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
</UPopover>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<script setup>
|
|
73
|
+
import { ref } from "vue";
|
|
74
|
+
defineProps({
|
|
75
|
+
email: { type: String, required: false },
|
|
76
|
+
displayName: { type: String, required: false }
|
|
77
|
+
});
|
|
78
|
+
const emit = defineEmits(["signed-out", "change-password"]);
|
|
79
|
+
const isOpen = ref(false);
|
|
80
|
+
const pending = ref(false);
|
|
81
|
+
const triggerButtonProps = {
|
|
82
|
+
block: true,
|
|
83
|
+
variant: "ghost",
|
|
84
|
+
color: "neutral",
|
|
85
|
+
class: "theme-btn h-[48px] w-[48px] p-0! flex flex-col items-center justify-center gap-1 py-1.5 bg-transparent border-[length:var(--md-border-width)] border-[color:var(--md-success)]/40 rounded-[var(--md-border-radius)] text-[var(--md-on-surface)] hover:bg-[var(--md-success)]/18 active:bg-[var(--md-success)]/28 transition-colors duration-150 shadow-none",
|
|
86
|
+
ui: {
|
|
87
|
+
base: "justify-center shadow-none"
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function onChangePassword() {
|
|
91
|
+
isOpen.value = false;
|
|
92
|
+
emit("change-password");
|
|
93
|
+
}
|
|
94
|
+
async function onSignOut() {
|
|
95
|
+
pending.value = true;
|
|
96
|
+
try {
|
|
97
|
+
await $fetch("/api/basic-auth/sign-out", {
|
|
98
|
+
method: "POST"
|
|
99
|
+
});
|
|
100
|
+
emit("signed-out");
|
|
101
|
+
} finally {
|
|
102
|
+
pending.value = false;
|
|
103
|
+
isOpen.value = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
email?: string;
|
|
3
|
+
displayName?: string;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
|
+
"signed-out": () => any;
|
|
7
|
+
"change-password": () => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
"onSigned-out"?: (() => any) | undefined;
|
|
10
|
+
"onChange-password"?: (() => any) | undefined;
|
|
11
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template v-if="isBasicAuthProvider">
|
|
3
|
+
<BasicAuthUserMenu
|
|
4
|
+
v-if="isSignedIn"
|
|
5
|
+
:email="sessionUser?.email"
|
|
6
|
+
:display-name="sessionUser?.displayName"
|
|
7
|
+
@signed-out="onSignedOut"
|
|
8
|
+
@change-password="changePasswordModalOpen = true"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<UButton
|
|
12
|
+
v-else
|
|
13
|
+
v-bind="loginButtonProps"
|
|
14
|
+
type="button"
|
|
15
|
+
aria-label="Login"
|
|
16
|
+
@click="signInModalOpen = true"
|
|
17
|
+
>
|
|
18
|
+
<template #default>
|
|
19
|
+
<span class="flex flex-col items-center gap-1 w-full">
|
|
20
|
+
<UIcon name="i-lucide-log-in" class="h-[18px] w-[18px]" />
|
|
21
|
+
<span class="text-[7px] uppercase tracking-wider whitespace-nowrap">
|
|
22
|
+
Login
|
|
23
|
+
</span>
|
|
24
|
+
</span>
|
|
25
|
+
</template>
|
|
26
|
+
</UButton>
|
|
27
|
+
|
|
28
|
+
<BasicAuthSignInModal
|
|
29
|
+
v-model="signInModalOpen"
|
|
30
|
+
@signed-in="onSignedIn"
|
|
31
|
+
@open-register="openRegisterFromSignIn"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<BasicAuthRegisterModal
|
|
35
|
+
v-model="registerModalOpen"
|
|
36
|
+
@registered="onRegistered"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<BasicAuthChangePasswordModal
|
|
40
|
+
v-model="changePasswordModalOpen"
|
|
41
|
+
:username="sessionUser?.email"
|
|
42
|
+
@updated="onPasswordUpdated"
|
|
43
|
+
/>
|
|
44
|
+
</template>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup>
|
|
48
|
+
import { computed, nextTick, onMounted, ref } from "vue";
|
|
49
|
+
import { BASIC_AUTH_PROVIDER_ID } from "../lib/constants";
|
|
50
|
+
import BasicAuthSignInModal from "./BasicAuthSignInModal.client.vue";
|
|
51
|
+
import BasicAuthRegisterModal from "./BasicAuthRegisterModal.client.vue";
|
|
52
|
+
import BasicAuthUserMenu from "./BasicAuthUserMenu.client.vue";
|
|
53
|
+
import BasicAuthChangePasswordModal from "./BasicAuthChangePasswordModal.client.vue";
|
|
54
|
+
const runtimeConfig = useRuntimeConfig();
|
|
55
|
+
const signInModalOpen = ref(false);
|
|
56
|
+
const registerModalOpen = ref(false);
|
|
57
|
+
const changePasswordModalOpen = ref(false);
|
|
58
|
+
let refreshRequest = null;
|
|
59
|
+
const session = ref(null);
|
|
60
|
+
async function fetchSessionPayload() {
|
|
61
|
+
return await $fetch("/api/auth/session", {
|
|
62
|
+
cache: "no-store"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function tryRefreshTokens() {
|
|
66
|
+
if (refreshRequest) {
|
|
67
|
+
return refreshRequest;
|
|
68
|
+
}
|
|
69
|
+
refreshRequest = (async () => {
|
|
70
|
+
try {
|
|
71
|
+
const response = await $fetch("/api/basic-auth/refresh?silent=1", {
|
|
72
|
+
method: "POST"
|
|
73
|
+
});
|
|
74
|
+
return response?.ok === true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
})().finally(() => {
|
|
79
|
+
refreshRequest = null;
|
|
80
|
+
});
|
|
81
|
+
return refreshRequest;
|
|
82
|
+
}
|
|
83
|
+
async function refreshSession(options = {}) {
|
|
84
|
+
const allowSilentRefresh = options.allowSilentRefresh ?? true;
|
|
85
|
+
try {
|
|
86
|
+
const payload = await fetchSessionPayload();
|
|
87
|
+
session.value = payload.session ?? null;
|
|
88
|
+
} catch {
|
|
89
|
+
session.value = null;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (session.value || !allowSilentRefresh) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const didRefresh = await tryRefreshTokens();
|
|
96
|
+
if (!didRefresh) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const payload = await fetchSessionPayload();
|
|
101
|
+
session.value = payload.session ?? null;
|
|
102
|
+
} catch {
|
|
103
|
+
session.value = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const isBasicAuthProvider = computed(() => {
|
|
107
|
+
const publicProvider = runtimeConfig.public?.authProvider;
|
|
108
|
+
if (!publicProvider) return true;
|
|
109
|
+
return publicProvider === BASIC_AUTH_PROVIDER_ID;
|
|
110
|
+
});
|
|
111
|
+
const isSignedIn = computed(
|
|
112
|
+
() => session.value?.authenticated === true && session.value.provider === BASIC_AUTH_PROVIDER_ID
|
|
113
|
+
);
|
|
114
|
+
const sessionUser = computed(() => session.value?.user);
|
|
115
|
+
const loginButtonProps = {
|
|
116
|
+
block: true,
|
|
117
|
+
variant: "ghost",
|
|
118
|
+
color: "neutral",
|
|
119
|
+
class: "theme-btn h-[48px] w-[48px] p-0! flex flex-col items-center justify-center gap-1 py-1.5 bg-transparent border-[length:var(--md-border-width)] border-[color:var(--md-primary)]/30 rounded-[var(--md-border-radius)] text-[var(--md-primary)] hover:bg-[var(--md-primary)]/15 active:bg-[var(--md-primary)]/25 transition-colors duration-150 shadow-none",
|
|
120
|
+
ui: {
|
|
121
|
+
base: "justify-center shadow-none"
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function notifyAuthSessionChanged() {
|
|
125
|
+
if (typeof window === "undefined") return;
|
|
126
|
+
window.dispatchEvent(new CustomEvent("or3:auth-session-changed"));
|
|
127
|
+
}
|
|
128
|
+
async function onSignedIn() {
|
|
129
|
+
await refreshSession({ allowSilentRefresh: false });
|
|
130
|
+
notifyAuthSessionChanged();
|
|
131
|
+
}
|
|
132
|
+
async function onSignedOut() {
|
|
133
|
+
await refreshSession({ allowSilentRefresh: false });
|
|
134
|
+
notifyAuthSessionChanged();
|
|
135
|
+
}
|
|
136
|
+
async function onPasswordUpdated() {
|
|
137
|
+
await refreshSession({ allowSilentRefresh: false });
|
|
138
|
+
notifyAuthSessionChanged();
|
|
139
|
+
}
|
|
140
|
+
async function openRegisterFromSignIn() {
|
|
141
|
+
signInModalOpen.value = false;
|
|
142
|
+
await nextTick();
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
registerModalOpen.value = true;
|
|
145
|
+
}, 0);
|
|
146
|
+
}
|
|
147
|
+
async function onRegistered() {
|
|
148
|
+
await refreshSession({ allowSilentRefresh: false });
|
|
149
|
+
notifyAuthSessionChanged();
|
|
150
|
+
}
|
|
151
|
+
onMounted(() => {
|
|
152
|
+
void refreshSession();
|
|
153
|
+
});
|
|
154
|
+
</script>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const BASIC_AUTH_PROVIDER_ID = "basic-auth";
|
|
2
|
+
export declare const ACCESS_COOKIE_NAME = "or3_access";
|
|
3
|
+
export declare const REFRESH_COOKIE_NAME = "or3_refresh";
|
|
4
|
+
export declare const DEFAULT_ACCESS_TTL_SECONDS = 900;
|
|
5
|
+
export declare const DEFAULT_REFRESH_TTL_SECONDS: number;
|
|
6
|
+
export declare const ACCESS_COOKIE_PATH = "/";
|
|
7
|
+
export declare const REFRESH_COOKIE_PATH = "/api/basic-auth";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const BASIC_AUTH_PROVIDER_ID = "basic-auth";
|
|
2
|
+
export const ACCESS_COOKIE_NAME = "or3_access";
|
|
3
|
+
export const REFRESH_COOKIE_NAME = "or3_refresh";
|
|
4
|
+
export const DEFAULT_ACCESS_TTL_SECONDS = 900;
|
|
5
|
+
export const DEFAULT_REFRESH_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
6
|
+
export const ACCESS_COOKIE_PATH = "/";
|
|
7
|
+
export const REFRESH_COOKIE_PATH = "/api/basic-auth";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { registerClientAuthStatusResolver } from "~/composables/auth/useClientAuthStatus.client";
|
|
2
|
+
async function fetchSession() {
|
|
3
|
+
try {
|
|
4
|
+
return await $fetch("/api/auth/session", {
|
|
5
|
+
cache: "no-store"
|
|
6
|
+
});
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
async function trySilentRefresh() {
|
|
12
|
+
try {
|
|
13
|
+
const res = await $fetch("/api/basic-auth/refresh?silent=1", {
|
|
14
|
+
method: "POST"
|
|
15
|
+
});
|
|
16
|
+
return res?.ok === true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export default defineNuxtPlugin(() => {
|
|
22
|
+
if (import.meta.server) return;
|
|
23
|
+
registerClientAuthStatusResolver(async () => {
|
|
24
|
+
const first = await fetchSession();
|
|
25
|
+
const firstSession = first?.session;
|
|
26
|
+
if (firstSession?.authenticated === true && firstSession.provider === "basic-auth") {
|
|
27
|
+
return { ready: true, authenticated: true };
|
|
28
|
+
}
|
|
29
|
+
const refreshed = await trySilentRefresh();
|
|
30
|
+
if (!refreshed) {
|
|
31
|
+
return { ready: true, authenticated: false };
|
|
32
|
+
}
|
|
33
|
+
const second = await fetchSession();
|
|
34
|
+
const secondSession = second?.session;
|
|
35
|
+
const authenticated = secondSession?.authenticated === true && secondSession.provider === "basic-auth";
|
|
36
|
+
return { ready: true, authenticated };
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from "#imports";
|
|
2
|
+
import { BASIC_AUTH_PROVIDER_ID } from "../lib/constants.js";
|
|
3
|
+
function tryRegisterAuthUiAdapter(component) {
|
|
4
|
+
const nuxtApp = useNuxtApp();
|
|
5
|
+
if (typeof nuxtApp.$registerAuthUiAdapter !== "function") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
nuxtApp.$registerAuthUiAdapter({
|
|
9
|
+
id: BASIC_AUTH_PROVIDER_ID,
|
|
10
|
+
component
|
|
11
|
+
});
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
function enqueueAuthUiAdapter(component) {
|
|
15
|
+
const payload = {
|
|
16
|
+
id: BASIC_AUTH_PROVIDER_ID,
|
|
17
|
+
component
|
|
18
|
+
};
|
|
19
|
+
const globalState = globalThis;
|
|
20
|
+
if (!Array.isArray(globalState.__or3AuthUiAdapterQueue__)) {
|
|
21
|
+
globalState.__or3AuthUiAdapterQueue__ = [];
|
|
22
|
+
}
|
|
23
|
+
globalState.__or3AuthUiAdapterQueue__.push(payload);
|
|
24
|
+
if (typeof window !== "undefined") {
|
|
25
|
+
window.dispatchEvent(
|
|
26
|
+
new CustomEvent("or3:auth-ui-adapter-register", {
|
|
27
|
+
detail: payload
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export default defineNuxtPlugin(async () => {
|
|
33
|
+
if (import.meta.server) return;
|
|
34
|
+
const runtimeConfig = useRuntimeConfig();
|
|
35
|
+
if (!runtimeConfig.public?.ssrAuthEnabled) return;
|
|
36
|
+
const publicProviderId = runtimeConfig.public?.authProvider;
|
|
37
|
+
if (publicProviderId && publicProviderId !== BASIC_AUTH_PROVIDER_ID) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const componentModule = await import("../components/SidebarAuthButtonBasic.client.vue");
|
|
41
|
+
if (componentModule.default) {
|
|
42
|
+
if (!tryRegisterAuthUiAdapter(componentModule.default)) {
|
|
43
|
+
enqueueAuthUiAdapter(componentModule.default);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BASIC_AUTH_PROVIDER_ID } from "../../../lib/constants.js";
|
|
2
|
+
import { validateBasicAuthConfig } from "../../lib/config.js";
|
|
3
|
+
export const basicAuthAdminAdapter = {
|
|
4
|
+
id: BASIC_AUTH_PROVIDER_ID,
|
|
5
|
+
kind: "auth",
|
|
6
|
+
async getStatus(_event, ctx) {
|
|
7
|
+
const diagnostics = validateBasicAuthConfig(useRuntimeConfig());
|
|
8
|
+
const warnings = [];
|
|
9
|
+
for (const message of diagnostics.warnings) {
|
|
10
|
+
warnings.push({ level: "warning", message });
|
|
11
|
+
}
|
|
12
|
+
for (const message of diagnostics.errors) {
|
|
13
|
+
warnings.push({ level: "error", message });
|
|
14
|
+
}
|
|
15
|
+
if (ctx.enabled && !diagnostics.isValid) {
|
|
16
|
+
warnings.push({
|
|
17
|
+
level: "error",
|
|
18
|
+
message: "Basic-auth configuration is invalid. Authentication may fail."
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
details: {
|
|
23
|
+
dbPath: diagnostics.config.dbPath,
|
|
24
|
+
jwtSecretConfigured: Boolean(diagnostics.config.jwtSecret),
|
|
25
|
+
refreshSecretConfigured: Boolean(diagnostics.config.refreshSecret),
|
|
26
|
+
bootstrapConfigured: Boolean(diagnostics.config.bootstrapEmail) && Boolean(diagnostics.config.bootstrapPassword),
|
|
27
|
+
accessTtlSeconds: diagnostics.config.accessTtlSeconds,
|
|
28
|
+
refreshTtlSeconds: diagnostics.config.refreshTtlSeconds
|
|
29
|
+
},
|
|
30
|
+
warnings,
|
|
31
|
+
actions: []
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createError, type H3Event } from 'h3';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
export declare function assertBasicAuthReady(event: H3Event): void;
|
|
4
|
+
export declare function parseBodyWithSchema<T extends z.ZodTypeAny>(event: H3Event, schema: T): Promise<z.infer<T>>;
|
|
5
|
+
export declare function noStore(event: H3Event): void;
|
|
6
|
+
export declare function createForbiddenError(): ReturnType<typeof createError>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createError, readBody, setResponseHeader } from "h3";
|
|
2
|
+
import { validateBasicAuthConfig } from "../../lib/config.js";
|
|
3
|
+
import { createAuthNotConfiguredError, createInvalidRequestError } from "../../lib/errors.js";
|
|
4
|
+
export function assertBasicAuthReady(event) {
|
|
5
|
+
const diagnostics = validateBasicAuthConfig(useRuntimeConfig(event));
|
|
6
|
+
if (!diagnostics.config.enabled || diagnostics.config.providerId !== "basic-auth") {
|
|
7
|
+
throw createAuthNotConfiguredError();
|
|
8
|
+
}
|
|
9
|
+
if (!diagnostics.isValid) {
|
|
10
|
+
throw createAuthNotConfiguredError();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function parseBodyWithSchema(event, schema) {
|
|
14
|
+
const body = await readBody(event);
|
|
15
|
+
const result = schema.safeParse(body);
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
throw createInvalidRequestError();
|
|
18
|
+
}
|
|
19
|
+
return result.data;
|
|
20
|
+
}
|
|
21
|
+
export function noStore(event) {
|
|
22
|
+
setResponseHeader(event, "Cache-Control", "no-store");
|
|
23
|
+
}
|
|
24
|
+
export function createForbiddenError() {
|
|
25
|
+
return createError({ statusCode: 403, statusMessage: "Forbidden" });
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function handleChangePassword(event: H3Event): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
}>>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineEventHandler } from "h3";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { clearAuthCookies } from "../../lib/cookies.js";
|
|
4
|
+
import { createInvalidCredentialsError, createSessionExpiredError } from "../../lib/errors.js";
|
|
5
|
+
import { hashPassword, verifyPassword } from "../../lib/password.js";
|
|
6
|
+
import { enforceBasicAuthRateLimit } from "../../lib/rate-limit.js";
|
|
7
|
+
import {
|
|
8
|
+
findAccountById,
|
|
9
|
+
updatePasswordAndRevokeSessions
|
|
10
|
+
} from "../../lib/session-store.js";
|
|
11
|
+
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
12
|
+
import { basicAuthProvider } from "../../auth/basic-auth-provider.js";
|
|
13
|
+
import { assertBasicAuthReady, noStore, parseBodyWithSchema } from "./_helpers.js";
|
|
14
|
+
const changePasswordSchema = z.object({
|
|
15
|
+
// Existing accounts may have legacy short passwords; only new password is length-enforced.
|
|
16
|
+
currentPassword: z.string().min(1).max(512),
|
|
17
|
+
newPassword: z.string().min(8).max(512),
|
|
18
|
+
confirmNewPassword: z.string().min(8).max(512)
|
|
19
|
+
}).refine((input) => input.newPassword === input.confirmNewPassword, {
|
|
20
|
+
message: "Passwords must match"
|
|
21
|
+
});
|
|
22
|
+
export async function handleChangePassword(event) {
|
|
23
|
+
assertBasicAuthReady(event);
|
|
24
|
+
noStore(event);
|
|
25
|
+
enforceMutationOriginPolicy(event);
|
|
26
|
+
enforceBasicAuthRateLimit(event, "basic-auth:change-password");
|
|
27
|
+
const session = await basicAuthProvider.getSession(event);
|
|
28
|
+
if (!session) {
|
|
29
|
+
clearAuthCookies(event);
|
|
30
|
+
throw createSessionExpiredError();
|
|
31
|
+
}
|
|
32
|
+
const body = await parseBodyWithSchema(event, changePasswordSchema);
|
|
33
|
+
const account = findAccountById(session.user.id);
|
|
34
|
+
if (!account) {
|
|
35
|
+
clearAuthCookies(event);
|
|
36
|
+
throw createSessionExpiredError();
|
|
37
|
+
}
|
|
38
|
+
const validCurrentPassword = await verifyPassword(body.currentPassword, account.password_hash);
|
|
39
|
+
if (!validCurrentPassword) {
|
|
40
|
+
throw createInvalidCredentialsError();
|
|
41
|
+
}
|
|
42
|
+
const newPasswordHash = await hashPassword(body.newPassword);
|
|
43
|
+
updatePasswordAndRevokeSessions(account.id, newPasswordHash);
|
|
44
|
+
clearAuthCookies(event);
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
export default defineEventHandler(async (event) => {
|
|
48
|
+
return await handleChangePassword(event);
|
|
49
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function handleRefresh(event: H3Event): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
}>>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { defineEventHandler, getQuery } from "h3";
|
|
3
|
+
import { clearAuthCookies, setAccessCookie, setRefreshCookie } from "../../lib/cookies.js";
|
|
4
|
+
import { getBasicAuthConfig } from "../../lib/config.js";
|
|
5
|
+
import { createSessionExpiredError } from "../../lib/errors.js";
|
|
6
|
+
import {
|
|
7
|
+
getRefreshTokenFromEvent,
|
|
8
|
+
hashRefreshToken,
|
|
9
|
+
signAccessToken,
|
|
10
|
+
signRefreshToken,
|
|
11
|
+
verifyRefreshToken
|
|
12
|
+
} from "../../lib/jwt.js";
|
|
13
|
+
import { enforceBasicAuthRateLimit } from "../../lib/rate-limit.js";
|
|
14
|
+
import {
|
|
15
|
+
findAccountById,
|
|
16
|
+
rotateSession,
|
|
17
|
+
getSessionMetadataFromEvent
|
|
18
|
+
} from "../../lib/session-store.js";
|
|
19
|
+
import { enforceMutationOriginPolicy } from "../../lib/request-security.js";
|
|
20
|
+
import { assertBasicAuthReady, noStore } from "./_helpers.js";
|
|
21
|
+
export async function handleRefresh(event) {
|
|
22
|
+
assertBasicAuthReady(event);
|
|
23
|
+
noStore(event);
|
|
24
|
+
enforceMutationOriginPolicy(event);
|
|
25
|
+
enforceBasicAuthRateLimit(event, "basic-auth:refresh");
|
|
26
|
+
const query = getQuery(event);
|
|
27
|
+
const silent = query.silent === "1" || query.silent === "true";
|
|
28
|
+
const failExpired = () => {
|
|
29
|
+
clearAuthCookies(event);
|
|
30
|
+
if (silent) {
|
|
31
|
+
return { ok: false };
|
|
32
|
+
}
|
|
33
|
+
throw createSessionExpiredError();
|
|
34
|
+
};
|
|
35
|
+
const refreshToken = getRefreshTokenFromEvent(event);
|
|
36
|
+
if (!refreshToken) {
|
|
37
|
+
return failExpired();
|
|
38
|
+
}
|
|
39
|
+
const claims = await verifyRefreshToken(refreshToken);
|
|
40
|
+
if (!claims) {
|
|
41
|
+
return failExpired();
|
|
42
|
+
}
|
|
43
|
+
const account = findAccountById(claims.sub);
|
|
44
|
+
if (!account || account.token_version !== claims.ver) {
|
|
45
|
+
return failExpired();
|
|
46
|
+
}
|
|
47
|
+
const config = getBasicAuthConfig();
|
|
48
|
+
const newSessionId = randomUUID();
|
|
49
|
+
const newRefreshToken = await signRefreshToken({
|
|
50
|
+
sub: account.id,
|
|
51
|
+
sid: newSessionId,
|
|
52
|
+
ver: account.token_version
|
|
53
|
+
});
|
|
54
|
+
const rotation = rotateSession({
|
|
55
|
+
currentSessionId: claims.sid,
|
|
56
|
+
currentRefreshHash: hashRefreshToken(refreshToken),
|
|
57
|
+
newSessionId,
|
|
58
|
+
newRefreshHash: hashRefreshToken(newRefreshToken),
|
|
59
|
+
newExpiresAtMs: Date.now() + config.refreshTtlSeconds * 1e3,
|
|
60
|
+
metadata: getSessionMetadataFromEvent(event)
|
|
61
|
+
});
|
|
62
|
+
if (!rotation.ok || rotation.accountId !== account.id) {
|
|
63
|
+
return failExpired();
|
|
64
|
+
}
|
|
65
|
+
const accessToken = await signAccessToken({
|
|
66
|
+
sub: account.id,
|
|
67
|
+
sid: newSessionId,
|
|
68
|
+
ver: account.token_version,
|
|
69
|
+
email: account.email,
|
|
70
|
+
display_name: account.display_name
|
|
71
|
+
});
|
|
72
|
+
setAccessCookie(event, accessToken, config.accessTtlSeconds);
|
|
73
|
+
setRefreshCookie(event, newRefreshToken, config.refreshTtlSeconds);
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
export default defineEventHandler(async (event) => {
|
|
77
|
+
return await handleRefresh(event);
|
|
78
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
export declare function handleRegister(event: H3Event): Promise<{
|
|
3
|
+
ok: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
}>>;
|
|
8
|
+
export default _default;
|