Re-login modal (#717)
* Implement quick login/register flow with global event handling - Add QuickRegister component with improved modal management - Integrate quick login/register with app store state - Implement custom event handling for login/registration flow - Update OAuth callback to support quick login in popup windows - Refactor authentication-related components to use global events * Refactor authentication flow with centralized useAuth composable - Create new useAuth composable to centralize login, registration, and social login logic - Simplify authentication methods in LoginForm and RegisterForm - Add event-based login/registration flow with quick login support - Remove redundant API calls and consolidate authentication processes - Improve error handling and analytics tracking for authentication events * Enhance QuickRegister and RegisterForm components with unauthorized error handling - Add closeable functionality to modals based on unauthorized error state - Implement logout button in QuickRegister for unauthorized users - Reset unauthorized error state on component unmount - Update styling for "OR" text in RegisterForm for consistency - Set unauthorized error flag in app store upon 401 response in API calls * Refactor Authentication Flow and Remove Unused Callback Views - Deleted unused callback views for Notion and OAuth to streamline the codebase. - Updated QuickRegister and LoginForm components to remove the after-login event emission, replacing it with a window message system for better communication between components. - Enhanced the RegisterForm and other components to utilize the new window message system for handling login completion, improving reliability and maintainability. - Added a verifyAuthentication method in the useAuth composable to ensure user data is loaded correctly after social logins, including retry logic for fetching user data. These changes aim to simplify the authentication process and improve the overall user experience by ensuring a more robust handling of login events. * Add eslint-disable comment to useWindowMessage composable for linting control * Refactor QuickRegister.vue for improved template structure and clarity - Adjusted the rendering of horizontal dividers and the "or" text for better semantic HTML. - Added a compact-header prop to the modal for enhanced layout control. These changes aim to enhance the readability and maintainability of the QuickRegister component. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
a711b961d4
commit
2c746437c9
|
|
@ -1,12 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ config('app.name') }}</title>
|
||||
<script>
|
||||
window.opener.postMessage(@json($result), "{{ url('/') }}")
|
||||
window.close()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ config('app.name') }}</title>
|
||||
<script>
|
||||
window.opener.postMessage({ token: "{{ $token }}" }, "{{ url('/') }}")
|
||||
window.close()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
@extends('errors.layout')
|
||||
|
||||
@section('title', 'Login Error')
|
||||
|
||||
@section('message', 'Email already taken.')
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
<NotificationsWrapper />
|
||||
<feature-base />
|
||||
<SubscriptionModal />
|
||||
<QuickRegister />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
this.form.seo_meta[keyname] = null
|
||||
})
|
||||
|
||||
if (this.form.custom_domain && !this.workspace.custom_domains.find((item) => { return item === this.form.custom_domain })) {
|
||||
if (this.form.custom_domain && this.workspace?.custom_domains && !this.workspace.custom_domains.find((item) => { return item === this.form.custom_domain })) {
|
||||
this.form.custom_domain = null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,8 +97,7 @@
|
|||
|
||||
<script>
|
||||
import ForgotPasswordModal from "../ForgotPasswordModal.vue"
|
||||
import { opnFetch } from "~/composables/useOpnApi.js"
|
||||
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
|
||||
import { WindowMessageTypes } from "~/composables/useWindowMessage"
|
||||
|
||||
export default {
|
||||
name: "LoginForm",
|
||||
|
|
@ -113,7 +112,7 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
emits: ['afterQuickLogin', 'openRegister'],
|
||||
emits: ['openRegister'],
|
||||
setup() {
|
||||
return {
|
||||
appStore: useAppStore(),
|
||||
|
|
@ -136,44 +135,38 @@ export default {
|
|||
|
||||
computed: {},
|
||||
|
||||
mounted() {
|
||||
// Use the window message composable
|
||||
const windowMessage = useWindowMessage(WindowMessageTypes.LOGIN_COMPLETE)
|
||||
|
||||
// Listen for login complete messages
|
||||
windowMessage.listen(() => {
|
||||
this.redirect()
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
login() {
|
||||
// Submit the form.
|
||||
async login() {
|
||||
this.loading = true
|
||||
this.form
|
||||
.post("login", { data: { remember: this.remember } })
|
||||
.then(async (data) => {
|
||||
// Save the token with its expiration time
|
||||
this.authStore.setToken(data.token, data.expires_in)
|
||||
|
||||
const [userDataResponse, workspacesResponse] = await Promise.all([
|
||||
opnFetch("user"),
|
||||
fetchAllWorkspaces(),
|
||||
])
|
||||
this.authStore.setUser(userDataResponse)
|
||||
this.workspaceStore.set(workspacesResponse.data.value)
|
||||
|
||||
// Load forms
|
||||
this.formsStore.loadAll(this.workspaceStore.currentId)
|
||||
|
||||
// Redirect home.
|
||||
const auth = useAuth()
|
||||
|
||||
try {
|
||||
await auth.loginWithCredentials(this.form, this.remember)
|
||||
this.redirect()
|
||||
} catch (error) {
|
||||
if (error.response?._data?.message == "You must change your credentials when in self host mode") {
|
||||
this.redirect()
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?._data?.message == "You must change your credentials when in self host mode") {
|
||||
// this.showForgotModal = true
|
||||
this.redirect()
|
||||
}
|
||||
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
redirect() {
|
||||
if (this.isQuick) {
|
||||
this.$emit("afterQuickLogin")
|
||||
// Use window message instead of event
|
||||
const afterLoginMessage = useWindowMessage(WindowMessageTypes.AFTER_LOGIN)
|
||||
afterLoginMessage.send(window)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
<div>
|
||||
<!-- Login modal -->
|
||||
<modal
|
||||
:show="showLoginModal"
|
||||
compact-header
|
||||
:show="appStore.quickLoginModal"
|
||||
max-width="lg"
|
||||
@close="showLoginModal = false"
|
||||
:closeable="!appStore.isUnauthorizedError"
|
||||
@close="appStore.quickLoginModal=false"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
|
|
@ -26,19 +28,50 @@
|
|||
Login to OpnForm
|
||||
</template>
|
||||
<div class="px-4">
|
||||
<template v-if="appStore.isUnauthorizedError">
|
||||
<div class="mb-4 p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700 rounded-md">
|
||||
<p class="text-amber-800 dark:text-amber-200 text-sm font-medium">
|
||||
Your session has expired. Please log in again to continue.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<login-form
|
||||
:is-quick="true"
|
||||
@open-register="openRegister"
|
||||
@after-quick-login="afterQuickLogin"
|
||||
/>
|
||||
|
||||
<template v-if="appStore.isUnauthorizedError">
|
||||
<div class="flex items-center my-6">
|
||||
<div class="h-[1px] bg-gray-300 dark:bg-gray-600 flex-1" />
|
||||
<div class="px-4 text-gray-500 text-sm">
|
||||
or
|
||||
</div>
|
||||
<div class="h-[1px] bg-gray-300 dark:bg-gray-600 flex-1" />
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-right-on-rectangle"
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="white"
|
||||
label="Logout"
|
||||
:block="true"
|
||||
size="lg"
|
||||
@click="logout"
|
||||
/>
|
||||
<p class="text-gray-500 text-sm text-center mt-2">
|
||||
Progress will be lost.
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
<!-- Register modal -->
|
||||
<modal
|
||||
:show="showRegisterModal"
|
||||
compact-header
|
||||
:show="appStore.quickRegisterModal"
|
||||
max-width="lg"
|
||||
@close="$emit('close')"
|
||||
:closeable="!appStore.isUnauthorizedError"
|
||||
@close="appStore.quickRegisterModal=false"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
|
|
@ -63,50 +96,100 @@
|
|||
<register-form
|
||||
:is-quick="true"
|
||||
@open-login="openLogin"
|
||||
@after-quick-login="afterQuickLogin"
|
||||
/>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import LoginForm from "./LoginForm.vue"
|
||||
import RegisterForm from "./RegisterForm.vue"
|
||||
import { WindowMessageTypes } from "~/composables/useWindowMessage"
|
||||
|
||||
export default {
|
||||
name: "QuickRegister",
|
||||
components: {
|
||||
LoginForm,
|
||||
RegisterForm,
|
||||
const appStore = useAppStore()
|
||||
const props = defineProps({
|
||||
redirectUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
props: {
|
||||
showRegisterModal: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['afterLogin', 'close', 'reopen'],
|
||||
})
|
||||
|
||||
data: () => ({
|
||||
showLoginModal: false,
|
||||
}),
|
||||
// Define emits for component interactions
|
||||
const emit = defineEmits(['close', 'reopen'])
|
||||
|
||||
mounted() {},
|
||||
const windowMessage = useWindowMessage(WindowMessageTypes.LOGIN_COMPLETE)
|
||||
|
||||
methods: {
|
||||
openLogin() {
|
||||
this.showLoginModal = true
|
||||
this.$emit("close")
|
||||
},
|
||||
openRegister() {
|
||||
this.showLoginModal = false
|
||||
this.$emit("reopen")
|
||||
},
|
||||
afterQuickLogin() {
|
||||
this.showLoginModal = false
|
||||
this.$emit("afterLogin")
|
||||
},
|
||||
},
|
||||
// Set up a listener for after-login messages
|
||||
const afterLoginMessage = useWindowMessage(WindowMessageTypes.AFTER_LOGIN)
|
||||
|
||||
onMounted(() => {
|
||||
// Listen for login-complete messages from popup window
|
||||
windowMessage.listen(() => {
|
||||
// Handle social login completion
|
||||
handleSocialLogin()
|
||||
})
|
||||
|
||||
// Set up after-login listener for component communication
|
||||
afterLoginMessage.listen(() => {
|
||||
afterQuickLogin()
|
||||
})
|
||||
|
||||
// Reset the unauthorized error flag when component is unmounted
|
||||
onUnmounted(() => {
|
||||
appStore.isUnauthorizedError = false
|
||||
})
|
||||
})
|
||||
|
||||
// Handle social login completion
|
||||
const handleSocialLogin = () => {
|
||||
// This function is only called for social logins, so we refresh tokens
|
||||
const authStore = useAuthStore()
|
||||
const tokenValue = useCookie("token").value
|
||||
const adminTokenValue = useCookie("admin_token").value
|
||||
|
||||
// Re-initialize the store with the latest tokens from cookies
|
||||
authStore.initStore(tokenValue, adminTokenValue)
|
||||
|
||||
// Then proceed with normal login flow
|
||||
afterQuickLogin()
|
||||
}
|
||||
|
||||
const openLogin = () => {
|
||||
appStore.quickLoginModal = true
|
||||
appStore.quickRegisterModal = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const openRegister = () => {
|
||||
appStore.quickLoginModal = false
|
||||
appStore.quickRegisterModal = true
|
||||
emit('reopen')
|
||||
}
|
||||
|
||||
const afterQuickLogin = async () => {
|
||||
// Reset unauthorized error flag immediately
|
||||
appStore.isUnauthorizedError = false
|
||||
|
||||
// Verify authentication is complete using the useAuth composable
|
||||
const auth = useAuth()
|
||||
await auth.verifyAuthentication()
|
||||
|
||||
// Close both login and register modals
|
||||
appStore.quickLoginModal = false
|
||||
appStore.quickRegisterModal = false
|
||||
|
||||
// Show success alert
|
||||
useAlert().success("Successfully logged in. Welcome back!")
|
||||
|
||||
// Use the window message for after-login instead of emitting the event
|
||||
afterLoginMessage.send(window, { useMessageChannel: false })
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
appStore.isUnauthorizedError = false
|
||||
appStore.quickLoginModal = false
|
||||
appStore.quickRegisterModal = false
|
||||
useRouter().push('/login')
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -104,8 +104,8 @@
|
|||
</v-button>
|
||||
|
||||
<template v-if="useFeatureFlag('services.google.auth')">
|
||||
<p class="text-gray-600/50 text-sm text-center my-4">
|
||||
Or
|
||||
<p class="text-gray-500 text-sm text-center my-4">
|
||||
OR
|
||||
</p>
|
||||
<v-button
|
||||
native-type="buttom"
|
||||
|
|
@ -140,8 +140,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {opnFetch} from "~/composables/useOpnApi.js"
|
||||
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
|
||||
import { WindowMessageTypes } from "~/composables/useWindowMessage"
|
||||
|
||||
export default {
|
||||
name: "RegisterForm",
|
||||
|
|
@ -153,7 +152,7 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['afterQuickLogin', 'openLogin'],
|
||||
emits: ['openLogin'],
|
||||
|
||||
setup() {
|
||||
const { $utm } = useNuxtApp()
|
||||
|
|
@ -209,6 +208,14 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
// Use the window message composable
|
||||
const windowMessage = useWindowMessage(WindowMessageTypes.LOGIN_COMPLETE)
|
||||
|
||||
// Listen for login complete messages
|
||||
windowMessage.listen(() => {
|
||||
this.redirect()
|
||||
})
|
||||
|
||||
// Set appsumo license
|
||||
if (
|
||||
this.$route.query.appsumo_license !== undefined &&
|
||||
|
|
@ -229,61 +236,27 @@ export default {
|
|||
|
||||
methods: {
|
||||
async register() {
|
||||
let data
|
||||
this.form.utm_data = this.$utm.value
|
||||
const auth = useAuth()
|
||||
|
||||
// Reset captcha after submission
|
||||
if (import.meta.client && this.recaptchaSiteKey) {
|
||||
this.$refs.captcha.reset()
|
||||
}
|
||||
|
||||
try {
|
||||
// Register the user.
|
||||
data = await this.form.post("/register")
|
||||
this.form.utm_data = this.$utm.value
|
||||
await auth.registerUser(this.form)
|
||||
|
||||
this.redirect()
|
||||
} catch (err) {
|
||||
useAlert().error(err.response?._data?.message)
|
||||
return false
|
||||
}
|
||||
|
||||
// Log in the user.
|
||||
const tokenData = await this.form.post("/login", { data: { remember: true } })
|
||||
|
||||
// Save the token with its expiration time.
|
||||
this.authStore.setToken(tokenData.token, tokenData.expires_in)
|
||||
|
||||
const userData = await opnFetch("user")
|
||||
this.authStore.setUser(userData)
|
||||
|
||||
const workspaces = await fetchAllWorkspaces()
|
||||
this.workspaceStore.set(workspaces.data.value)
|
||||
|
||||
// Load forms
|
||||
this.formsStore.loadAll(this.workspaceStore.currentId)
|
||||
|
||||
this.logEvent("register", {source: this.form.hear_about_us})
|
||||
try {
|
||||
useGtm().trackEvent({
|
||||
event: 'register',
|
||||
source: this.form.hear_about_us
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
// AppSumo License
|
||||
if (data.appsumo_license === false) {
|
||||
useAlert().error(
|
||||
"Invalid AppSumo license. This probably happened because this license was already" +
|
||||
" attached to another OpnForm account. Please contact support.",
|
||||
)
|
||||
} else if (data.appsumo_license === true) {
|
||||
useAlert().success(
|
||||
"Your AppSumo license was successfully activated! You now have access to all the" +
|
||||
" features of the AppSumo deal.",
|
||||
)
|
||||
}
|
||||
|
||||
// Redirect
|
||||
},
|
||||
redirect() {
|
||||
if (this.isQuick) {
|
||||
this.$emit("afterQuickLogin")
|
||||
// Use window message instead of event
|
||||
const afterLoginMessage = useWindowMessage(WindowMessageTypes.AFTER_LOGIN)
|
||||
afterLoginMessage.send(window)
|
||||
} else {
|
||||
// If is invite just redirect to home
|
||||
if (this.form.invite_token) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
export const useAuth = () => {
|
||||
const authStore = useAuthStore()
|
||||
const workspaceStore = useWorkspacesStore()
|
||||
const formsStore = useFormsStore()
|
||||
const logEvent = useAmplitude().logEvent
|
||||
|
||||
/**
|
||||
* Core authentication logic used by both social and direct login
|
||||
*/
|
||||
const authenticateUser = async ({ tokenData, source, isNewUser = false }) => {
|
||||
// Set token first
|
||||
authStore.setToken(tokenData.token, tokenData.expires_in)
|
||||
|
||||
// Fetch initial data
|
||||
const [userData, workspaces] = await Promise.all([
|
||||
opnFetch("user"),
|
||||
fetchAllWorkspaces()
|
||||
])
|
||||
|
||||
// Setup stores
|
||||
authStore.setUser(userData)
|
||||
workspaceStore.set(workspaces.data.value)
|
||||
|
||||
// Load forms for current workspace
|
||||
await formsStore.loadAll(workspaceStore.currentId)
|
||||
|
||||
// Track analytics
|
||||
const eventName = isNewUser ? 'register' : 'login'
|
||||
logEvent(eventName, { source })
|
||||
|
||||
try {
|
||||
// Check if GTM is available before using it
|
||||
const gtm = typeof useGtm === 'function' ? useGtm() : null
|
||||
if (gtm && typeof gtm.trackEvent === 'function') {
|
||||
gtm.trackEvent({
|
||||
event: eventName,
|
||||
source
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return { userData, workspaces, isNewUser }
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that authentication is complete and user data is loaded
|
||||
* Useful for social auth flows where token might be set but user data not loaded yet
|
||||
*/
|
||||
const verifyAuthentication = async () => {
|
||||
// If we already have user data, no need to verify
|
||||
if (authStore.check) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we have a token but no user data, fetch the user data
|
||||
if (authStore.token && !authStore.check) {
|
||||
// Create a promise with retry logic
|
||||
return new Promise((resolve, reject) => {
|
||||
const maxRetries = 3
|
||||
let retryCount = 0
|
||||
|
||||
const attemptFetch = async () => {
|
||||
try {
|
||||
const userData = await opnFetch("user")
|
||||
|
||||
if (userData) {
|
||||
authStore.setUser(userData)
|
||||
resolve(true)
|
||||
} else {
|
||||
handleRetry("No user data returned")
|
||||
}
|
||||
} catch (error) {
|
||||
handleRetry(`Auth verification failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = (reason) => {
|
||||
retryCount++
|
||||
if (retryCount < maxRetries) {
|
||||
console.log(`Retrying auth verification (${retryCount}/${maxRetries}): ${reason}`)
|
||||
// Exponential backoff
|
||||
setTimeout(attemptFetch, 100 * Math.pow(2, retryCount))
|
||||
} else {
|
||||
console.error(`Auth verification failed after ${maxRetries} attempts`)
|
||||
reject(new Error(`Auth verification failed after ${maxRetries} attempts`))
|
||||
}
|
||||
}
|
||||
|
||||
// Start the first attempt
|
||||
attemptFetch()
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle direct login with form validation
|
||||
*/
|
||||
const loginWithCredentials = async (form, remember) => {
|
||||
const tokenData = await form.submit('post', '/login', { data: { remember: remember } })
|
||||
|
||||
return authenticateUser({
|
||||
tokenData,
|
||||
source: 'credentials'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle social login callback
|
||||
*/
|
||||
const handleSocialCallback = async (provider, code, utmData) => {
|
||||
const tokenData = await opnFetch(`/oauth/${provider}/callback`, {
|
||||
method: 'POST',
|
||||
body: { code, utm_data: utmData }
|
||||
})
|
||||
|
||||
return authenticateUser({
|
||||
tokenData,
|
||||
source: provider,
|
||||
isNewUser: tokenData.new_user
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user registration
|
||||
*/
|
||||
const registerUser = async (form) => {
|
||||
// Register the user first
|
||||
const data = await form.submit('post', '/register')
|
||||
|
||||
// Login the user
|
||||
const tokenData = await form.submit('post', '/login')
|
||||
|
||||
const result = await authenticateUser({
|
||||
tokenData,
|
||||
source: form.hear_about_us,
|
||||
isNewUser: true
|
||||
})
|
||||
|
||||
// Handle AppSumo license if present
|
||||
if (data.appsumo_license === false) {
|
||||
useAlert().error(
|
||||
"Invalid AppSumo license. This probably happened because this license was already" +
|
||||
" attached to another OpnForm account. Please contact support."
|
||||
)
|
||||
} else if (data.appsumo_license === true) {
|
||||
useAlert().success(
|
||||
"Your AppSumo license was successfully activated! You now have access to all the" +
|
||||
" features of the AppSumo deal."
|
||||
)
|
||||
}
|
||||
|
||||
return { ...result, data }
|
||||
}
|
||||
|
||||
return {
|
||||
loginWithCredentials,
|
||||
handleSocialCallback,
|
||||
registerUser,
|
||||
verifyAuthentication
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +63,8 @@ export function getOpnRequestsOptions(request, opts) {
|
|||
if (authStore.check) {
|
||||
console.log("Logging out due to 401")
|
||||
authStore.logout()
|
||||
useRouter().push({ name: "login" })
|
||||
useAppStore().isUnauthorizedError = true
|
||||
useAppStore().quickLoginModal = true
|
||||
}
|
||||
} else if (status === 420) {
|
||||
// If invalid domain, redirect to main domain
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
/* eslint-disable */
|
||||
/**
|
||||
* Composable for handling window message communication
|
||||
* Provides a consistent interface for cross-window/iframe communication
|
||||
*/
|
||||
|
||||
// Define common message types as constants
|
||||
export const WindowMessageTypes = {
|
||||
LOGIN_COMPLETE: 'login-complete',
|
||||
AFTER_LOGIN: 'after-login'
|
||||
}
|
||||
|
||||
export const useWindowMessage = (messageType = null) => {
|
||||
const listeners = ref(new Map())
|
||||
|
||||
/**
|
||||
* Derives an acknowledgment message name from the original message name
|
||||
* @param {string} originalMessage - The original message name
|
||||
* @returns {string} - The derived acknowledgment message name
|
||||
*/
|
||||
const deriveAcknowledgmentName = (originalMessage) => {
|
||||
return `${originalMessage}-acknowledged`
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener for a specific message type
|
||||
*
|
||||
* @param {string} eventType - The type of message to listen for (defaults to constructor value)
|
||||
* @param {Function} callback - The callback function to call when the message is received
|
||||
* @param {Object} options - Options for the listener
|
||||
* @param {boolean} options.useMessageChannel - Whether to expect and use MessageChannel ports in the event
|
||||
* @param {boolean} options.acknowledge - Whether to automatically acknowledge the message
|
||||
*/
|
||||
const listen = (callback, options = {}, eventType = null) => {
|
||||
const targetEventType = eventType || messageType
|
||||
if (!targetEventType) {
|
||||
console.error('No message type provided to listen for')
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
useMessageChannel = true,
|
||||
acknowledge = true
|
||||
} = options
|
||||
|
||||
const acknowledgmentName = deriveAcknowledgmentName(targetEventType)
|
||||
|
||||
const handler = (event) => {
|
||||
// For simple messages
|
||||
if (!useMessageChannel && event.data === targetEventType) {
|
||||
callback(event)
|
||||
return
|
||||
}
|
||||
|
||||
// For MessageChannel messages
|
||||
if (useMessageChannel && event.data === targetEventType && event.ports && event.ports.length > 0) {
|
||||
// Send acknowledgement if requested
|
||||
if (acknowledge && event.ports[0]) {
|
||||
event.ports[0].postMessage(acknowledgmentName)
|
||||
}
|
||||
|
||||
// Call the callback with the event
|
||||
callback(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add the listener to the window
|
||||
window.addEventListener('message', handler)
|
||||
|
||||
// Store the handler for cleanup
|
||||
listeners.value.set(targetEventType, handler)
|
||||
|
||||
// Return a function to remove the listener
|
||||
return () => stopListening(targetEventType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener for a specific message type
|
||||
*
|
||||
* @param {string} eventType - The type of message to stop listening for
|
||||
*/
|
||||
const stopListening = (eventType = null) => {
|
||||
const targetEventType = eventType || messageType
|
||||
if (!targetEventType) {
|
||||
console.error('No message type provided to stop listening for')
|
||||
return
|
||||
}
|
||||
|
||||
const handler = listeners.value.get(targetEventType)
|
||||
if (handler) {
|
||||
window.removeEventListener('message', handler)
|
||||
listeners.value.delete(targetEventType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to another window
|
||||
*
|
||||
* @param {Window} targetWindow - The window to send the message to
|
||||
* @param {Object} options - Options for sending the message
|
||||
* @param {string} options.eventType - The type of message to send (defaults to constructor value)
|
||||
* @param {string} options.targetOrigin - The origin to send the message to, defaults to '*'
|
||||
* @param {boolean} options.useMessageChannel - Whether to use MessageChannel for communication
|
||||
* @param {number} options.timeout - Timeout in ms for the acknowledgment, defaults to 500ms
|
||||
* @param {boolean} options.waitForAcknowledgment - Whether to wait for acknowledgment
|
||||
* @returns {Promise} - Resolves when acknowledged or after timeout
|
||||
*/
|
||||
const send = (targetWindow, options = {}) => {
|
||||
const {
|
||||
eventType = null,
|
||||
targetOrigin = '*',
|
||||
useMessageChannel = true,
|
||||
timeout = 500,
|
||||
waitForAcknowledgment = true
|
||||
} = options
|
||||
|
||||
const targetEventType = eventType || messageType
|
||||
if (!targetEventType) {
|
||||
console.error('No message type provided to send')
|
||||
return Promise.reject(new Error('No message type provided'))
|
||||
}
|
||||
|
||||
const acknowledgmentName = deriveAcknowledgmentName(targetEventType)
|
||||
|
||||
if (!useMessageChannel) {
|
||||
// Simple message without MessageChannel
|
||||
targetWindow.postMessage(targetEventType, targetOrigin)
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
// Using MessageChannel for two-way communication
|
||||
return new Promise((resolve) => {
|
||||
// Create a message channel for two-way communication
|
||||
const channel = new MessageChannel()
|
||||
|
||||
// If we expect an acknowledgment, listen for it
|
||||
if (waitForAcknowledgment) {
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data === acknowledgmentName) {
|
||||
resolve(true) // Acknowledged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message with the port
|
||||
targetWindow.postMessage(targetEventType, targetOrigin, [channel.port2])
|
||||
|
||||
// Set a timeout as fallback
|
||||
if (waitForAcknowledgment) {
|
||||
setTimeout(() => resolve(false), timeout)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all listeners
|
||||
* Call this explicitly if needed, though it's automatically called on unmount
|
||||
*/
|
||||
const cleanup = () => {
|
||||
listeners.value.forEach((handler, type) => {
|
||||
window.removeEventListener('message', handler)
|
||||
})
|
||||
listeners.value.clear()
|
||||
}
|
||||
|
||||
// Auto-cleanup when the component is unmounted
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// If messageType was provided on creation, set up a default listener
|
||||
const setupDefaultListener = (callback, options = {}) => {
|
||||
if (messageType && callback) {
|
||||
return listen(callback, options)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
listen,
|
||||
stopListening,
|
||||
send,
|
||||
cleanup,
|
||||
setupDefaultListener
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
class="w-full flex flex-grow"
|
||||
:error="error"
|
||||
:is-guest="isGuest"
|
||||
@open-register="registerModal = true"
|
||||
@open-register="appStore.quickRegisterModal = true"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
|
|
@ -19,25 +19,18 @@
|
|||
>
|
||||
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
|
||||
|
||||
<quick-register
|
||||
:show-register-modal="registerModal"
|
||||
@close="registerModal = false"
|
||||
@reopen="registerModal = true"
|
||||
@after-login="afterLogin"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FormEditor from "~/components/open/forms/components/FormEditor.vue"
|
||||
import QuickRegister from "~/components/pages/auth/components/QuickRegister.vue"
|
||||
import CreateFormBaseModal from "../../../components/pages/forms/create/CreateFormBaseModal.vue"
|
||||
import { initForm } from "~/composables/forms/initForm.js"
|
||||
import { fetchTemplate } from "~/stores/templates.js"
|
||||
import { fetchAllWorkspaces } from "~/stores/workspaces.js"
|
||||
import { WindowMessageTypes } from "~/composables/useWindowMessage"
|
||||
|
||||
const appStore = useAppStore()
|
||||
const templatesStore = useTemplatesStore()
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
|
|
@ -63,7 +56,6 @@ definePageMeta({
|
|||
// Data
|
||||
const stateReady = ref(false)
|
||||
const error = ref("")
|
||||
const registerModal = ref(false)
|
||||
const isGuest = ref(true)
|
||||
const showInitialFormModal = ref(false)
|
||||
|
||||
|
|
@ -92,10 +84,15 @@ onMounted(() => {
|
|||
showInitialFormModal.value = true
|
||||
}
|
||||
stateReady.value = true
|
||||
|
||||
// Set up window message listener for after-login
|
||||
const afterLoginMessage = useWindowMessage(WindowMessageTypes.AFTER_LOGIN)
|
||||
afterLoginMessage.listen(() => {
|
||||
afterLogin()
|
||||
}, { useMessageChannel: false })
|
||||
})
|
||||
|
||||
const afterLogin = () => {
|
||||
registerModal.value = false
|
||||
isGuest.value = false
|
||||
fetchAllWorkspaces()
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
class="m-6 flex flex-col items-center space-y-4"
|
||||
>
|
||||
<p class="text-center">
|
||||
Unable to sign it at the moment.
|
||||
Unable to sign in at the moment.
|
||||
</p>
|
||||
<v-button
|
||||
:to="{ name: 'login' }"
|
||||
|
|
@ -29,74 +29,77 @@
|
|||
|
||||
<script setup>
|
||||
import { useNuxtApp } from "nuxt/app"
|
||||
import { WindowMessageTypes } from "~/composables/useWindowMessage"
|
||||
|
||||
const { $utm } = useNuxtApp()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
const formsStore = useFormsStore()
|
||||
const logEvent = useAmplitude().logEvent
|
||||
const loading = ref(true)
|
||||
const form = useForm({
|
||||
code: '',
|
||||
utm_data: null,
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
alias: '/oauth/:provider/callback'
|
||||
})
|
||||
|
||||
function handleCallback() {
|
||||
const handleCallback = async () => {
|
||||
const auth = useAuth()
|
||||
const provider = route.params.provider
|
||||
|
||||
try {
|
||||
const { isNewUser } = await auth.handleSocialCallback(
|
||||
provider,
|
||||
route.query.code,
|
||||
$utm.value
|
||||
)
|
||||
|
||||
const provider = route.params.provider
|
||||
form.code = route.query.code
|
||||
form.utm_data = $utm.value
|
||||
form.post(`/oauth/${provider}/callback`).then(async (data) => {
|
||||
authStore.setToken(data.token, data.expires_in)
|
||||
const [userDataResponse, workspacesResponse] = await Promise.all([
|
||||
opnFetch("user"),
|
||||
fetchAllWorkspaces(),
|
||||
])
|
||||
authStore.setUser(userDataResponse)
|
||||
workspacesStore.set(workspacesResponse.data.value)
|
||||
|
||||
// Load forms
|
||||
formsStore.loadAll(workspacesStore.currentId)
|
||||
if (!data.new_user) {
|
||||
logEvent("login", { source: provider })
|
||||
try {
|
||||
useGtm().trackEvent({
|
||||
event: 'login',
|
||||
source: provider
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
router.push({ name: "home" })
|
||||
return
|
||||
} else {
|
||||
logEvent("register", { source: provider })
|
||||
router.push({ name: "forms-create" })
|
||||
useAlert().success("Success! You're now registered with your Google account! Welcome to OpnForm.")
|
||||
try {
|
||||
useGtm().trackEvent({
|
||||
event: 'register',
|
||||
source: provider
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
useAlert().error(error)
|
||||
}
|
||||
if (!isNewUser) {
|
||||
// Handle existing user login
|
||||
if (window.opener) {
|
||||
try {
|
||||
// Use the WindowMessage composable for more reliable communication
|
||||
const windowMessage = useWindowMessage(WindowMessageTypes.LOGIN_COMPLETE)
|
||||
|
||||
// Send the login-complete message and wait for acknowledgment
|
||||
await windowMessage.send(window.opener, {
|
||||
waitForAcknowledgment: true,
|
||||
timeout: 500
|
||||
})
|
||||
|
||||
// Now we can safely close the window
|
||||
window.close()
|
||||
|
||||
// If window doesn't close (some browsers prevent it), show a message
|
||||
loading.value = false
|
||||
} catch (err) {
|
||||
console.error("Error in social callback:", err)
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(error => {
|
||||
useAlert().error(error.response._data.message)
|
||||
loading.value = false
|
||||
})
|
||||
} else {
|
||||
// No opener, redirect to home
|
||||
router.push({ name: "home" })
|
||||
}
|
||||
} else {
|
||||
// Handle new user registration
|
||||
router.push({ name: "forms-create" })
|
||||
useAlert().success("Success! You're now registered with your Google account! Welcome to OpnForm.")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Social login error:", error)
|
||||
useAlert().error(error.response?._data?.message || "Authentication failed")
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
handleCallback()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Set a timeout to ensure we don't get stuck in loading state
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (loading.value) {
|
||||
loading.value = false
|
||||
console.error("Social login timed out")
|
||||
}
|
||||
}, 10000) // 10 second timeout
|
||||
|
||||
handleCallback().finally(() => {
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
@ -9,7 +9,10 @@ export const useAppStore = defineStore("app", {
|
|||
chatOpened: false,
|
||||
hidden: false
|
||||
},
|
||||
|
||||
isUnauthorizedError: false,
|
||||
quickLoginModal: false,
|
||||
quickRegisterModal: false,
|
||||
|
||||
// App Loader
|
||||
loader: {
|
||||
percent: 0,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
|
|||
}
|
||||
})
|
||||
.then((data) => {
|
||||
window.location.href = data.url
|
||||
window.open(data.url, '_blank')
|
||||
})
|
||||
.catch((error) => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue