or3-provider-basic-auth 0.0.10 → 0.0.11
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 +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/BasicAuthLockPage.client.vue +8 -1
- package/dist/runtime/components/BasicAuthRegisterModal.client.d.vue.ts +2 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue +10 -3
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue.d.ts +2 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.vue +9 -2
- package/dist/runtime/lib/invite-link.client.d.ts +4 -0
- package/dist/runtime/lib/invite-link.client.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Strict-mode behavior:
|
|
|
69
69
|
- Canonical OR3 users/workspaces are still resolved by the selected `AuthWorkspaceStore`.
|
|
70
70
|
- In `invite_only` mode, signed-token, expiry, persisted invite state/token hash, and normalized-email validation runs before Basic Auth creates an account or session.
|
|
71
71
|
- The selected `AuthWorkspaceStore` must support atomic invite provisioning; internal user/auth mapping, membership, and invite consumption are committed together by that provider.
|
|
72
|
+
- Invite links (`/?invite=TOKEN`, copied from Admin → Workspaces, or carried in the lock page's `next`) open registration for signed-out visitors with the token filled in. The token is removed from the address bar on first read and kept in memory for that page; only one mounted auth control (sidebar, mobile More sheet or lock page) opens registration per link. On mobile the account control lives in More, so registration opens when the visitor opens More. This invite-link handoff requires published `or3-provider-basic-auth@0.0.11` or later.
|
|
72
73
|
- Basic Auth account and initial refresh-session rows are created in one local SQLite transaction.
|
|
73
74
|
- Access JWTs are short-lived and validated by `basicAuthProvider.getSession(event)`.
|
|
74
75
|
- Refresh tokens are rotated and hashed at rest; replay attempts revoke active sessions.
|
package/dist/module.json
CHANGED
|
@@ -23,18 +23,25 @@
|
|
|
23
23
|
|
|
24
24
|
<BasicAuthRegisterModal
|
|
25
25
|
v-model="registerModalOpen"
|
|
26
|
+
:invite-token="inviteToken"
|
|
26
27
|
@registered="handleRegistered"
|
|
27
28
|
/>
|
|
28
29
|
</div>
|
|
29
30
|
</template>
|
|
30
31
|
|
|
31
32
|
<script setup>
|
|
32
|
-
import { nextTick, ref } from "vue";
|
|
33
|
+
import { nextTick, onMounted, ref } from "vue";
|
|
33
34
|
import BasicAuthRegisterModal from "./BasicAuthRegisterModal.client.vue";
|
|
34
35
|
import BasicAuthSignInModal from "./BasicAuthSignInModal.client.vue";
|
|
36
|
+
import { claimInviteLinkAutoOpen, readInviteLinkToken } from "../lib/invite-link.client";
|
|
35
37
|
const loginIcon = "i-lucide-log-in";
|
|
36
38
|
const signInModalOpen = ref(false);
|
|
37
39
|
const registerModalOpen = ref(false);
|
|
40
|
+
const inviteToken = ref(null);
|
|
41
|
+
onMounted(() => {
|
|
42
|
+
inviteToken.value = readInviteLinkToken();
|
|
43
|
+
if (claimInviteLinkAutoOpen()) registerModalOpen.value = true;
|
|
44
|
+
});
|
|
38
45
|
function notifyAuthSessionChanged() {
|
|
39
46
|
if (typeof window === "undefined") return;
|
|
40
47
|
window.dispatchEvent(new CustomEvent("or3:auth-session-changed"));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
|
+
/** Token from an invite link; filled in when the form opens. */
|
|
4
|
+
inviteToken?: string | null;
|
|
3
5
|
};
|
|
4
6
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
7
|
"update:modelValue": (value: boolean) => any;
|
|
@@ -73,7 +73,10 @@
|
|
|
73
73
|
/>
|
|
74
74
|
</UFormField>
|
|
75
75
|
|
|
76
|
-
<UFormField
|
|
76
|
+
<UFormField
|
|
77
|
+
:label="props.inviteToken ? 'Invite token (from your invite link)' : 'Invite token (optional)'"
|
|
78
|
+
name="inviteToken"
|
|
79
|
+
>
|
|
77
80
|
<UInput
|
|
78
81
|
v-model="state.inviteToken"
|
|
79
82
|
type="text"
|
|
@@ -111,14 +114,15 @@
|
|
|
111
114
|
</template>
|
|
112
115
|
|
|
113
116
|
<script setup>
|
|
114
|
-
import { computed, reactive, ref } from "vue";
|
|
117
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
115
118
|
import { newPasswordFitsBcrypt } from "../password-policy";
|
|
116
119
|
const modalUi = {
|
|
117
120
|
overlay: "basic-auth-modal-overlay",
|
|
118
121
|
content: "basic-auth-modal-content sm:max-w-[420px]"
|
|
119
122
|
};
|
|
120
123
|
const props = defineProps({
|
|
121
|
-
modelValue: { type: Boolean, required: true }
|
|
124
|
+
modelValue: { type: Boolean, required: true },
|
|
125
|
+
inviteToken: { type: [String, null], required: false }
|
|
122
126
|
});
|
|
123
127
|
const emit = defineEmits(["update:modelValue", "registered"]);
|
|
124
128
|
const state = reactive({
|
|
@@ -136,6 +140,9 @@ const isOpen = computed({
|
|
|
136
140
|
emit("update:modelValue", value);
|
|
137
141
|
}
|
|
138
142
|
});
|
|
143
|
+
watch(() => [props.modelValue, props.inviteToken], ([open, token]) => {
|
|
144
|
+
if (open && token && !state.inviteToken.trim()) state.inviteToken = token;
|
|
145
|
+
}, { immediate: true });
|
|
139
146
|
function close() {
|
|
140
147
|
errorMessage.value = "";
|
|
141
148
|
emit("update:modelValue", false);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
|
+
/** Token from an invite link; filled in when the form opens. */
|
|
4
|
+
inviteToken?: string | null;
|
|
3
5
|
};
|
|
4
6
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
7
|
"update:modelValue": (value: boolean) => any;
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<BasicAuthRegisterModal
|
|
59
59
|
v-model="registerModalOpen"
|
|
60
|
+
:invite-token="inviteToken"
|
|
60
61
|
@registered="onRegistered"
|
|
61
62
|
/>
|
|
62
63
|
|
|
@@ -76,6 +77,7 @@ import {
|
|
|
76
77
|
AUTH_UI_LAYOUT_KEY
|
|
77
78
|
} from "../lib/auth-ui-layout";
|
|
78
79
|
import { silentRefreshOnce } from "../lib/silent-refresh.client";
|
|
80
|
+
import { claimInviteLinkAutoOpen, readInviteLinkToken } from "../lib/invite-link.client";
|
|
79
81
|
import BasicAuthSignInModal from "./BasicAuthSignInModal.client.vue";
|
|
80
82
|
import BasicAuthRegisterModal from "./BasicAuthRegisterModal.client.vue";
|
|
81
83
|
import BasicAuthUserMenu from "./BasicAuthUserMenu.client.vue";
|
|
@@ -171,8 +173,13 @@ async function onRegistered() {
|
|
|
171
173
|
await refreshSession({ allowSilentRefresh: false });
|
|
172
174
|
notifyAuthSessionChanged();
|
|
173
175
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const inviteToken = ref(null);
|
|
177
|
+
onMounted(async () => {
|
|
178
|
+
inviteToken.value = readInviteLinkToken();
|
|
179
|
+
await refreshSession();
|
|
180
|
+
if (!isSignedIn.value && isBasicAuthProvider.value && claimInviteLinkAutoOpen()) {
|
|
181
|
+
registerModalOpen.value = true;
|
|
182
|
+
}
|
|
176
183
|
});
|
|
177
184
|
</script>
|
|
178
185
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
let token;
|
|
2
|
+
let autoOpenClaimed = false;
|
|
3
|
+
function takeInvite(url) {
|
|
4
|
+
const direct = url.searchParams.get("invite")?.trim();
|
|
5
|
+
if (direct) {
|
|
6
|
+
url.searchParams.delete("invite");
|
|
7
|
+
return direct;
|
|
8
|
+
}
|
|
9
|
+
const next = url.searchParams.get("next");
|
|
10
|
+
if (!next) return null;
|
|
11
|
+
const target = new URL(next, url.origin);
|
|
12
|
+
const carried = target.searchParams.get("invite")?.trim();
|
|
13
|
+
if (!carried) return null;
|
|
14
|
+
target.searchParams.delete("invite");
|
|
15
|
+
url.searchParams.set("next", target.pathname + target.search + target.hash);
|
|
16
|
+
return carried;
|
|
17
|
+
}
|
|
18
|
+
export function readInviteLinkToken() {
|
|
19
|
+
if (token !== void 0) return token;
|
|
20
|
+
token = null;
|
|
21
|
+
if (typeof window === "undefined") return token;
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(window.location.href);
|
|
24
|
+
token = takeInvite(url);
|
|
25
|
+
if (token) window.history.replaceState(window.history.state, "", url.pathname + url.search + url.hash);
|
|
26
|
+
} catch {
|
|
27
|
+
token = null;
|
|
28
|
+
}
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
export function claimInviteLinkAutoOpen() {
|
|
32
|
+
if (!readInviteLinkToken() || autoOpenClaimed) return false;
|
|
33
|
+
autoOpenClaimed = true;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
export function resetInviteLinkForTests() {
|
|
37
|
+
token = void 0;
|
|
38
|
+
autoOpenClaimed = false;
|
|
39
|
+
}
|