@jskit-ai/auth-web 0.1.4
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/package.descriptor.mjs +290 -0
- package/package.json +29 -0
- package/src/client/composables/useDefaultLoginView.js +935 -0
- package/src/client/composables/useDefaultSignOutView.js +113 -0
- package/src/client/index.js +19 -0
- package/src/client/lib/returnToPath.js +20 -0
- package/src/client/lib/surfaceLinkTarget.js +19 -0
- package/src/client/providers/AuthWebClientProvider.js +72 -0
- package/src/client/runtime/authGuardRuntime.js +499 -0
- package/src/client/runtime/authHttpClient.js +19 -0
- package/src/client/runtime/inject.js +43 -0
- package/src/client/runtime/tokens.js +7 -0
- package/src/client/runtime/useLoginView.js +7 -0
- package/src/client/runtime/useSignOut.js +121 -0
- package/src/client/views/AuthProfileMenuLinkItem.vue +83 -0
- package/src/client/views/AuthProfileWidget.vue +100 -0
- package/src/client/views/DefaultLoginView.vue +291 -0
- package/src/client/views/DefaultSignOutView.vue +58 -0
- package/src/server/constants/authActionIds.js +15 -0
- package/src/server/controllers/AuthController.js +183 -0
- package/src/server/providers/AuthRouteServiceProvider.js +31 -0
- package/src/server/providers/AuthWebServiceProvider.js +23 -0
- package/src/server/routes/authRoutes.js +244 -0
- package/src/server/services/AuthWebService.js +126 -0
- package/templates/src/pages/auth/login.vue +17 -0
- package/templates/src/pages/auth/signout.vue +17 -0
- package/templates/src/runtime/authGuardRuntime.js +7 -0
- package/templates/src/runtime/authHttpClient.js +1 -0
- package/templates/src/runtime/useSignOut.js +1 -0
- package/templates/src/views/auth/LoginView.vue +7 -0
- package/templates/src/views/auth/SignOutView.vue +7 -0
- package/test/authGuardRuntime.test.js +361 -0
- package/test/clientBoot.test.js +16 -0
- package/test/clientSurface.test.js +89 -0
- package/test/index.test.js +21 -0
- package/test/logoutFallback.test.js +50 -0
- package/test/providerRuntime.test.js +100 -0
- package/test/returnToPath.test.js +72 -0
- package/test/surfaceLinkTarget.test.js +80 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { mdiAccountCogOutline, mdiCogOutline, mdiLogin, mdiLogout } from "@mdi/js";
|
|
4
|
+
import {
|
|
5
|
+
useWebPlacementContext,
|
|
6
|
+
resolveSurfaceNavigationTargetFromPlacementContext
|
|
7
|
+
} from "@jskit-ai/shell-web/client/placement";
|
|
8
|
+
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
label: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: ""
|
|
13
|
+
},
|
|
14
|
+
to: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: ""
|
|
17
|
+
},
|
|
18
|
+
icon: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: ""
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
24
|
+
|
|
25
|
+
const resolvedNavigationTarget = computed(() => {
|
|
26
|
+
const target = String(props.to || "").trim();
|
|
27
|
+
if (!target) {
|
|
28
|
+
return {
|
|
29
|
+
href: "",
|
|
30
|
+
sameOrigin: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
35
|
+
path: target
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
href: navigationTarget.href,
|
|
39
|
+
sameOrigin: navigationTarget.sameOrigin
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const resolvedIcon = computed(() => {
|
|
44
|
+
const explicitIcon = String(props.icon || "").trim();
|
|
45
|
+
if (explicitIcon) {
|
|
46
|
+
return explicitIcon;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalizedLabel = String(props.label || "").trim().toLowerCase();
|
|
50
|
+
const normalizedTarget = String(props.to || "").trim().toLowerCase();
|
|
51
|
+
if (
|
|
52
|
+
normalizedLabel.includes("sign in") ||
|
|
53
|
+
normalizedTarget.includes("/auth/login")
|
|
54
|
+
) {
|
|
55
|
+
return mdiLogin;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
normalizedLabel.includes("sign out") ||
|
|
60
|
+
normalizedTarget.includes("/auth/signout")
|
|
61
|
+
) {
|
|
62
|
+
return mdiLogout;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (normalizedLabel.includes("settings") || normalizedTarget.includes("/settings")) {
|
|
66
|
+
if (normalizedTarget.includes("/account")) {
|
|
67
|
+
return mdiAccountCogOutline;
|
|
68
|
+
}
|
|
69
|
+
return mdiCogOutline;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return "";
|
|
73
|
+
});
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<v-list-item
|
|
78
|
+
:title="props.label || undefined"
|
|
79
|
+
:to="resolvedNavigationTarget.sameOrigin ? resolvedNavigationTarget.href || undefined : undefined"
|
|
80
|
+
:href="resolvedNavigationTarget.sameOrigin ? undefined : resolvedNavigationTarget.href || undefined"
|
|
81
|
+
:prepend-icon="resolvedIcon || undefined"
|
|
82
|
+
/>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
3
|
+
import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
4
|
+
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
5
|
+
import { useAuthGuardRuntime } from "../runtime/inject.js";
|
|
6
|
+
|
|
7
|
+
const authGuardRuntime = useAuthGuardRuntime({
|
|
8
|
+
required: true
|
|
9
|
+
});
|
|
10
|
+
const authState = ref(authGuardRuntime.getState());
|
|
11
|
+
const { context: shellPlacementContext } = useWebPlacementContext();
|
|
12
|
+
let unsubscribe = null;
|
|
13
|
+
|
|
14
|
+
const shellUser = computed(() => {
|
|
15
|
+
const user = shellPlacementContext.value?.user;
|
|
16
|
+
if (!user || typeof user !== "object") {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
return user;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const displayName = computed(() => {
|
|
23
|
+
const fromContext = String(shellUser.value.displayName || shellUser.value.name || "").trim();
|
|
24
|
+
if (fromContext) {
|
|
25
|
+
return fromContext;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const username = String(authState.value?.username || "").trim();
|
|
29
|
+
if (username) {
|
|
30
|
+
return username;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return "Guest";
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const avatarUrl = computed(() => {
|
|
37
|
+
const value = String(shellUser.value.avatarUrl || shellUser.value.avatar || "").trim();
|
|
38
|
+
return value;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const initials = computed(() => {
|
|
42
|
+
const text = String(displayName.value || "").trim();
|
|
43
|
+
if (!text) {
|
|
44
|
+
return "G";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const words = text
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.map((entry) => entry.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
|
|
52
|
+
if (words.length > 1) {
|
|
53
|
+
return `${words[0][0]}${words[1][0]}`.toUpperCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return text.slice(0, 2).toUpperCase();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const placementContext = computed(() => {
|
|
60
|
+
return {
|
|
61
|
+
auth: authState.value,
|
|
62
|
+
user: shellUser.value
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
onMounted(() => {
|
|
67
|
+
unsubscribe = authGuardRuntime.subscribe((nextState) => {
|
|
68
|
+
authState.value = nextState;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
onBeforeUnmount(() => {
|
|
73
|
+
if (typeof unsubscribe === "function") {
|
|
74
|
+
unsubscribe();
|
|
75
|
+
unsubscribe = null;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<v-menu location="bottom end" offset="10">
|
|
82
|
+
<template #activator="{ props }">
|
|
83
|
+
<v-btn v-bind="props" variant="text" class="text-none pl-1 pr-2">
|
|
84
|
+
<v-avatar size="32" color="primary" variant="tonal">
|
|
85
|
+
<v-img v-if="avatarUrl" :src="avatarUrl" cover />
|
|
86
|
+
<span v-else class="text-caption font-weight-medium">{{ initials }}</span>
|
|
87
|
+
</v-avatar>
|
|
88
|
+
<span class="ml-2 d-none d-sm-inline text-body-2">{{ displayName }}</span>
|
|
89
|
+
</v-btn>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<v-list min-width="220" density="comfortable" class="py-1">
|
|
93
|
+
<ShellOutlet
|
|
94
|
+
host="auth-profile-menu"
|
|
95
|
+
position="primary-menu"
|
|
96
|
+
:context="placementContext"
|
|
97
|
+
/>
|
|
98
|
+
</v-list>
|
|
99
|
+
</v-menu>
|
|
100
|
+
</template>
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-main class="login-main">
|
|
3
|
+
<v-container class="fill-height d-flex align-center justify-center py-8">
|
|
4
|
+
<v-card class="auth-card" rounded="lg" elevation="1" border>
|
|
5
|
+
<v-card-text class="pa-7">
|
|
6
|
+
<div class="auth-header d-flex align-start justify-space-between ga-3 mb-5">
|
|
7
|
+
<div>
|
|
8
|
+
<p class="auth-kicker">Jskit Workspace</p>
|
|
9
|
+
<h1 class="auth-title">{{ authTitle }}</h1>
|
|
10
|
+
<p v-if="authSubtitle" class="text-medium-emphasis mb-0">{{ authSubtitle }}</p>
|
|
11
|
+
</div>
|
|
12
|
+
<v-chip color="primary" size="small" label>Secure</v-chip>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div v-if="!isForgot && !isOtp" class="mode-switch d-flex ga-2 pa-1 mb-5">
|
|
16
|
+
<v-btn
|
|
17
|
+
data-testid="auth-mode-sign-in"
|
|
18
|
+
class="text-none"
|
|
19
|
+
:variant="isLogin ? 'flat' : 'text'"
|
|
20
|
+
:color="isLogin ? 'primary' : undefined"
|
|
21
|
+
@click="switchMode('login')"
|
|
22
|
+
>
|
|
23
|
+
Sign in
|
|
24
|
+
</v-btn>
|
|
25
|
+
<v-btn
|
|
26
|
+
v-if="!showRememberedAccount"
|
|
27
|
+
data-testid="auth-mode-register"
|
|
28
|
+
class="text-none"
|
|
29
|
+
:variant="isRegister ? 'flat' : 'text'"
|
|
30
|
+
:color="isRegister ? 'primary' : undefined"
|
|
31
|
+
@click="switchMode('register')"
|
|
32
|
+
>
|
|
33
|
+
Register
|
|
34
|
+
</v-btn>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<v-form @submit.prevent="submitAuth" novalidate>
|
|
38
|
+
<div
|
|
39
|
+
v-if="showRememberedAccount"
|
|
40
|
+
class="remembered-account d-flex align-center justify-space-between ga-3 mb-4"
|
|
41
|
+
>
|
|
42
|
+
<div class="remembered-copy flex-grow-1">
|
|
43
|
+
<p class="remembered-title">Welcome back, {{ rememberedAccountDisplayName }}</p>
|
|
44
|
+
<p class="remembered-email">{{ rememberedAccountMaskedEmail }}</p>
|
|
45
|
+
</div>
|
|
46
|
+
<v-btn variant="text" color="secondary" class="text-none" @click="switchAccount">
|
|
47
|
+
{{ rememberedAccountSwitchLabel }}
|
|
48
|
+
</v-btn>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<v-text-field
|
|
52
|
+
v-model="email"
|
|
53
|
+
label="Email"
|
|
54
|
+
variant="outlined"
|
|
55
|
+
density="comfortable"
|
|
56
|
+
type="email"
|
|
57
|
+
autocomplete="email"
|
|
58
|
+
:error-messages="emailErrorMessages"
|
|
59
|
+
@blur="emailTouched = true"
|
|
60
|
+
class="mb-3"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<v-text-field
|
|
64
|
+
v-if="!isForgot && !isOtp"
|
|
65
|
+
v-model="password"
|
|
66
|
+
label="Password"
|
|
67
|
+
:type="showPassword ? 'text' : 'password'"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
density="comfortable"
|
|
70
|
+
:autocomplete="isRegister ? 'new-password' : 'current-password'"
|
|
71
|
+
:error-messages="passwordErrorMessages"
|
|
72
|
+
:append-inner-icon="showPassword ? '$eyeOff' : '$eye'"
|
|
73
|
+
@click:append-inner="showPassword = !showPassword"
|
|
74
|
+
@blur="passwordTouched = true"
|
|
75
|
+
class="mb-3"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<v-text-field
|
|
79
|
+
v-if="isRegister"
|
|
80
|
+
v-model="confirmPassword"
|
|
81
|
+
label="Confirm password"
|
|
82
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
83
|
+
variant="outlined"
|
|
84
|
+
density="comfortable"
|
|
85
|
+
autocomplete="new-password"
|
|
86
|
+
:error-messages="confirmPasswordErrorMessages"
|
|
87
|
+
:append-inner-icon="showConfirmPassword ? '$eyeOff' : '$eye'"
|
|
88
|
+
@click:append-inner="showConfirmPassword = !showConfirmPassword"
|
|
89
|
+
@blur="confirmPasswordTouched = true"
|
|
90
|
+
class="mb-3"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<v-text-field
|
|
94
|
+
v-if="isOtp"
|
|
95
|
+
v-model="otpCode"
|
|
96
|
+
label="One-time code"
|
|
97
|
+
variant="outlined"
|
|
98
|
+
density="comfortable"
|
|
99
|
+
autocomplete="one-time-code"
|
|
100
|
+
:error-messages="otpCodeErrorMessages"
|
|
101
|
+
@blur="otpCodeTouched = true"
|
|
102
|
+
class="mb-3"
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
<div v-if="isLogin" class="aux-links d-flex justify-end mb-4">
|
|
106
|
+
<v-btn variant="text" color="secondary" @click="switchMode('forgot')">Forgot password?</v-btn>
|
|
107
|
+
<v-btn variant="text" color="secondary" @click="switchMode('otp')">Use one-time code</v-btn>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<v-checkbox
|
|
111
|
+
v-if="isLogin || isOtp"
|
|
112
|
+
v-model="rememberAccountOnDevice"
|
|
113
|
+
label="Remember this account on this device"
|
|
114
|
+
density="compact"
|
|
115
|
+
hide-details
|
|
116
|
+
class="mb-4"
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<div v-if="isOtp" class="aux-links d-flex justify-end mb-4">
|
|
120
|
+
<v-btn
|
|
121
|
+
type="button"
|
|
122
|
+
variant="tonal"
|
|
123
|
+
color="secondary"
|
|
124
|
+
:loading="otpRequestPending"
|
|
125
|
+
@click="requestOtpCode"
|
|
126
|
+
>
|
|
127
|
+
Send one-time code
|
|
128
|
+
</v-btn>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div v-if="isLogin || isRegister" class="oauth-actions d-grid ga-2 mb-4">
|
|
132
|
+
<v-btn
|
|
133
|
+
v-for="provider in oauthProviders"
|
|
134
|
+
:key="provider.id"
|
|
135
|
+
block
|
|
136
|
+
variant="outlined"
|
|
137
|
+
color="secondary"
|
|
138
|
+
:disabled="loading"
|
|
139
|
+
:prepend-icon="oauthProviderIcon(provider)"
|
|
140
|
+
class="text-none oauth-provider-button"
|
|
141
|
+
@click="startOAuthSignIn(provider.id)"
|
|
142
|
+
>
|
|
143
|
+
{{ oauthProviderButtonLabel(provider) }}
|
|
144
|
+
</v-btn>
|
|
145
|
+
</div>
|
|
146
|
+
<v-btn
|
|
147
|
+
data-testid="auth-submit"
|
|
148
|
+
block
|
|
149
|
+
color="primary"
|
|
150
|
+
size="large"
|
|
151
|
+
:loading="loading"
|
|
152
|
+
:disabled="!canSubmit"
|
|
153
|
+
type="submit"
|
|
154
|
+
>
|
|
155
|
+
{{ submitLabel }}
|
|
156
|
+
</v-btn>
|
|
157
|
+
|
|
158
|
+
<div v-if="isRegister" class="switch-row mt-4 d-flex align-center justify-space-between ga-3">
|
|
159
|
+
<span class="text-medium-emphasis">Already have an account?</span>
|
|
160
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div v-else-if="isForgot" class="switch-row mt-4 d-flex align-center justify-space-between ga-3">
|
|
164
|
+
<span class="text-medium-emphasis">Remembered your password?</span>
|
|
165
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div v-else-if="isOtp" class="switch-row mt-4 d-flex align-center justify-space-between ga-3">
|
|
169
|
+
<span class="text-medium-emphasis">Want to use another method?</span>
|
|
170
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
171
|
+
</div>
|
|
172
|
+
</v-form>
|
|
173
|
+
</v-card-text>
|
|
174
|
+
</v-card>
|
|
175
|
+
</v-container>
|
|
176
|
+
</v-main>
|
|
177
|
+
</template>
|
|
178
|
+
|
|
179
|
+
<script setup>
|
|
180
|
+
import { useDefaultLoginView } from "../composables/useDefaultLoginView.js";
|
|
181
|
+
|
|
182
|
+
const {
|
|
183
|
+
authTitle,
|
|
184
|
+
authSubtitle,
|
|
185
|
+
isForgot,
|
|
186
|
+
isOtp,
|
|
187
|
+
isLogin,
|
|
188
|
+
isRegister,
|
|
189
|
+
showRememberedAccount,
|
|
190
|
+
switchMode,
|
|
191
|
+
submitAuth,
|
|
192
|
+
rememberedAccountDisplayName,
|
|
193
|
+
rememberedAccountMaskedEmail,
|
|
194
|
+
rememberedAccountSwitchLabel,
|
|
195
|
+
switchAccount,
|
|
196
|
+
email,
|
|
197
|
+
emailErrorMessages,
|
|
198
|
+
emailTouched,
|
|
199
|
+
password,
|
|
200
|
+
showPassword,
|
|
201
|
+
passwordErrorMessages,
|
|
202
|
+
passwordTouched,
|
|
203
|
+
confirmPassword,
|
|
204
|
+
showConfirmPassword,
|
|
205
|
+
confirmPasswordErrorMessages,
|
|
206
|
+
confirmPasswordTouched,
|
|
207
|
+
otpCode,
|
|
208
|
+
otpCodeErrorMessages,
|
|
209
|
+
otpCodeTouched,
|
|
210
|
+
rememberAccountOnDevice,
|
|
211
|
+
otpRequestPending,
|
|
212
|
+
requestOtpCode,
|
|
213
|
+
oauthProviders,
|
|
214
|
+
loading,
|
|
215
|
+
oauthProviderIcon,
|
|
216
|
+
startOAuthSignIn,
|
|
217
|
+
oauthProviderButtonLabel,
|
|
218
|
+
canSubmit,
|
|
219
|
+
submitLabel
|
|
220
|
+
} = useDefaultLoginView();
|
|
221
|
+
</script>
|
|
222
|
+
|
|
223
|
+
<style scoped>
|
|
224
|
+
.login-main {
|
|
225
|
+
background-color: rgb(var(--v-theme-background));
|
|
226
|
+
background-image: radial-gradient(circle at 15% 12%, rgba(0, 107, 83, 0.12), transparent 32%);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.auth-card {
|
|
230
|
+
width: min(520px, 100%);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.auth-kicker {
|
|
234
|
+
margin: 0 0 8px;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
letter-spacing: 0.09em;
|
|
238
|
+
text-transform: uppercase;
|
|
239
|
+
color: #395447;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.auth-title {
|
|
243
|
+
margin: 0 0 8px;
|
|
244
|
+
font-size: clamp(28px, 3vw, 32px);
|
|
245
|
+
line-height: 1.2;
|
|
246
|
+
color: #1d2c24;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.mode-switch {
|
|
250
|
+
border-radius: 12px;
|
|
251
|
+
background-color: rgba(57, 84, 71, 0.08);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.remembered-account {
|
|
255
|
+
border-radius: 12px;
|
|
256
|
+
border: 1px solid rgba(57, 84, 71, 0.2);
|
|
257
|
+
background: rgba(57, 84, 71, 0.07);
|
|
258
|
+
padding: 12px 14px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.remembered-copy {
|
|
262
|
+
min-width: 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.remembered-title {
|
|
266
|
+
margin: 0;
|
|
267
|
+
font-size: 14px;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
color: #1d2c24;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.remembered-email {
|
|
273
|
+
margin: 2px 0 0;
|
|
274
|
+
font-size: 12px;
|
|
275
|
+
color: rgba(29, 44, 36, 0.72);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.oauth-provider-button {
|
|
279
|
+
justify-content: center;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (max-width: 959px) {
|
|
283
|
+
.auth-content {
|
|
284
|
+
padding: 24px 20px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.auth-title {
|
|
288
|
+
font-size: 26px;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-main class="signout-main">
|
|
3
|
+
<v-container class="fill-height d-flex align-center justify-center py-8">
|
|
4
|
+
<v-card class="signout-card" rounded="lg" elevation="1" border>
|
|
5
|
+
<v-card-text class="pa-7">
|
|
6
|
+
<h1 class="text-h5 mb-3">Signing you out</h1>
|
|
7
|
+
|
|
8
|
+
<p v-if="status === 'pending'" class="text-medium-emphasis mb-4">
|
|
9
|
+
Please wait while we end your session.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p v-if="status === 'error'" class="text-body-1 text-medium-emphasis mb-4">
|
|
13
|
+
{{ errorMessage || "Sign out failed. Please try again." }}
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<div class="d-flex ga-3">
|
|
17
|
+
<v-btn
|
|
18
|
+
v-if="status === 'error'"
|
|
19
|
+
color="primary"
|
|
20
|
+
variant="flat"
|
|
21
|
+
class="text-none"
|
|
22
|
+
@click="retrySignOut"
|
|
23
|
+
>
|
|
24
|
+
Retry sign out
|
|
25
|
+
</v-btn>
|
|
26
|
+
<v-btn
|
|
27
|
+
v-if="status === 'error'"
|
|
28
|
+
color="secondary"
|
|
29
|
+
variant="text"
|
|
30
|
+
class="text-none"
|
|
31
|
+
@click="goToLogin"
|
|
32
|
+
>
|
|
33
|
+
Go to login
|
|
34
|
+
</v-btn>
|
|
35
|
+
<v-progress-circular v-if="status === 'pending'" indeterminate color="primary" size="22" />
|
|
36
|
+
</div>
|
|
37
|
+
</v-card-text>
|
|
38
|
+
</v-card>
|
|
39
|
+
</v-container>
|
|
40
|
+
</v-main>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup>
|
|
44
|
+
import { useDefaultSignOutView } from "../composables/useDefaultSignOutView.js";
|
|
45
|
+
|
|
46
|
+
const { status, errorMessage, retrySignOut, goToLogin } = useDefaultSignOutView();
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<style scoped>
|
|
50
|
+
.signout-main {
|
|
51
|
+
background-color: rgb(var(--v-theme-background));
|
|
52
|
+
background-image: radial-gradient(circle at 15% 12%, rgba(0, 107, 83, 0.12), transparent 32%);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.signout-card {
|
|
56
|
+
width: min(520px, 100%);
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const AUTH_ACTION_IDS = Object.freeze({
|
|
2
|
+
REGISTER: "auth.register",
|
|
3
|
+
LOGIN_PASSWORD: "auth.login.password",
|
|
4
|
+
LOGIN_OTP_REQUEST: "auth.login.otp.request",
|
|
5
|
+
LOGIN_OTP_VERIFY: "auth.login.otp.verify",
|
|
6
|
+
LOGIN_OAUTH_START: "auth.login.oauth.start",
|
|
7
|
+
LOGIN_OAUTH_COMPLETE: "auth.login.oauth.complete",
|
|
8
|
+
LOGOUT: "auth.logout",
|
|
9
|
+
SESSION_READ: "auth.session.read",
|
|
10
|
+
PASSWORD_RESET_REQUEST: "auth.password.reset.request",
|
|
11
|
+
PASSWORD_RECOVERY_COMPLETE: "auth.password.recovery.complete",
|
|
12
|
+
PASSWORD_RESET: "auth.password.reset"
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export { AUTH_ACTION_IDS };
|