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:
Chirag Chhatrala
2025-03-25 15:11:11 +05:30
committed by GitHub
parent a711b961d4
commit 2c746437c9
15 changed files with 602 additions and 224 deletions

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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) {