-
Notifications
You must be signed in to change notification settings - Fork 81
feat(auth): add Firebase email/password login/signup flow while keeping Google sign-in #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,15 @@ import Vue from 'vue' | |
| import VueRouter from 'vue-router' | ||
| // import store from '@/store/index' | ||
| import LandingPage from '@/views/LandingPage.vue' | ||
| import Auth from '@/views/Auth.vue' | ||
| // import Login from '@/views/Login' | ||
| import Dashboard from '@/views/Dashboard' | ||
| import Calibration from '@/views/CalibrationCard' | ||
| import CameraConfig from '@/views/CameraConfiguration' | ||
| import DoubleCalibrationRecord from '@/views/DoubleCalibrationRecord' | ||
| import PostCalibration from '@/views/PostCalibration' | ||
| import CalibrationConfig from '@/views/CalibrationConfig' | ||
| import { getCurrentUser } from '@/services/firebaseAuth' | ||
|
|
||
| Vue.use(VueRouter) | ||
|
|
||
|
|
@@ -18,6 +20,11 @@ const routes = [ | |
| name: 'LandingPage', | ||
| component: LandingPage, | ||
| }, | ||
| { | ||
| path: '/auth', | ||
| name: 'Auth', | ||
| component: Auth, | ||
| }, | ||
| // { | ||
| // path: '/login', | ||
| // name: 'Login', | ||
|
|
@@ -61,13 +68,34 @@ const router = new VueRouter({ | |
| routes | ||
| }) | ||
|
|
||
| // router.beforeResolve(async (to, from, next) => { | ||
| // var user = store.state.auth.user | ||
| // user = user ?? await store.dispatch('autoSignIn') | ||
| // if ((to.path == '/login' || to.path == '/') && user) next('/dashboard') | ||
| // else if ((to.path == '/dashboard') && !user) next('/login') | ||
| router.beforeEach(async (to, from, next) => { | ||
| if (to.path !== '/' && to.path !== '/auth') { | ||
| next() | ||
| return | ||
| } | ||
|
Comment on lines
+71
to
+75
|
||
|
|
||
| try { | ||
| const user = await getCurrentUser() | ||
|
|
||
| if (to.path === '/' && !user) { | ||
| next('/auth') | ||
| return | ||
| } | ||
|
|
||
| // next() | ||
| // }) | ||
| if (to.path === '/auth' && user) { | ||
| next('/dashboard') | ||
| return | ||
| } | ||
|
|
||
| next() | ||
| } catch (error) { | ||
| if (to.path === '/') { | ||
| next('/auth') | ||
| return | ||
| } | ||
|
|
||
| next() | ||
| } | ||
| }) | ||
|
|
||
| export default router | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import firebase from 'firebase/app' | ||
| import 'firebase/auth' | ||
| import 'firebase/firestore' | ||
|
|
||
| const mapUser = (user) => { | ||
| if (!user) { | ||
| return null | ||
| } | ||
|
|
||
| const { uid, email, displayName } = user | ||
| return { uid, email, displayName } | ||
| } | ||
|
|
||
| const saveUserProfile = async (user) => { | ||
| if (!user) { | ||
| return | ||
| } | ||
|
|
||
| const userProfile = { | ||
| displayName: user.displayName || user.email || '', | ||
| email: user.email || '', | ||
| } | ||
|
|
||
| await firebase | ||
| .firestore() | ||
| .collection('users') | ||
| .doc(user.uid) | ||
| .set(userProfile, { merge: true }) | ||
| } | ||
|
|
||
| export const getCurrentUser = () => { | ||
| return new Promise((resolve, reject) => { | ||
| const auth = firebase.auth() | ||
|
|
||
| if (auth.currentUser) { | ||
| resolve(mapUser(auth.currentUser)) | ||
| return | ||
| } | ||
|
|
||
| const unsubscribe = auth.onAuthStateChanged( | ||
| (user) => { | ||
| unsubscribe() | ||
| resolve(mapUser(user)) | ||
| }, | ||
| (error) => { | ||
PanditG4303 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| unsubscribe() | ||
| reject(error) | ||
| } | ||
| ) | ||
| }) | ||
| } | ||
|
|
||
| export const signInWithGoogle = async () => { | ||
| const provider = new firebase.auth.GoogleAuthProvider() | ||
| const result = await firebase.auth().signInWithPopup(provider) | ||
| const user = result.user | ||
|
|
||
| await saveUserProfile(user) | ||
|
|
||
|
Comment on lines
+53
to
+59
|
||
| return mapUser(user) | ||
| } | ||
|
|
||
| export const signInWithEmail = async (email, password) => { | ||
| const result = await firebase.auth().signInWithEmailAndPassword(email, password) | ||
| return mapUser(result.user) | ||
| } | ||
|
|
||
| export const signUpWithEmail = async (email, password) => { | ||
| const result = await firebase.auth().createUserWithEmailAndPassword(email, password) | ||
|
|
||
| await saveUserProfile(result.user) | ||
|
|
||
| return mapUser(result.user) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| <template> | ||
| <div style="height: 100%"> | ||
| <toolbar /> | ||
| <v-row justify="center" align="center" style="height: 90%"> | ||
| <v-col cols="12" lg="6" md="7"> | ||
| <v-card outlined class="py-12 px-6"> | ||
| <h2 class="text-center">{{ isSignup ? 'Sign Up' : 'Login' }}</h2> | ||
| <p class="text-center pt-6"> | ||
| {{ isSignup ? 'Create your account to start calibration sessions.' : 'Login to continue with your calibration sessions.' }} | ||
| </p> | ||
|
|
||
| <v-alert v-if="errorMessage" type="error" dense outlined class="mt-4 mb-0"> | ||
| {{ errorMessage }} | ||
| </v-alert> | ||
|
|
||
| <v-text-field | ||
| id="auth-email" | ||
| v-model="email" | ||
| label="Email" | ||
| type="email" | ||
| autocomplete="email" | ||
| outlined | ||
| dense | ||
| class="mt-6" | ||
| :disabled="loading" | ||
| /> | ||
|
|
||
| <v-text-field | ||
| id="auth-password" | ||
| v-model="password" | ||
| label="Password" | ||
| type="password" | ||
| autocomplete="current-password" | ||
| outlined | ||
| dense | ||
| :disabled="loading" | ||
| @keyup.enter="submitAuth" | ||
|
Comment on lines
+28
to
+37
|
||
| /> | ||
|
|
||
| <v-btn | ||
| id="auth-submit" | ||
| color="black" | ||
| dark | ||
| block | ||
| :loading="loading" | ||
| :disabled="loading" | ||
| @click="submitAuth" | ||
| > | ||
| {{ isSignup ? 'Sign up' : 'Login' }} | ||
| </v-btn> | ||
|
|
||
| <v-row justify="center" class="mt-6"> | ||
| <v-btn | ||
| id="login-btn-google" | ||
| large | ||
| tile | ||
| class="rounded-0 ma-0" | ||
| color="#ffffff" | ||
| :disabled="loading" | ||
| @click="loginWithGoogle" | ||
| > | ||
| <v-img | ||
| height="35px" | ||
| width="35px" | ||
| class="signin-icon" | ||
| src="@/assets/iconGoogle.svg" | ||
| alt="Google sign in - icon" | ||
| /> | ||
| <span class="pl-1 pr-1 ma-0 text-center text-capitalize">Sign in with Google</span> | ||
| </v-btn> | ||
| </v-row> | ||
|
|
||
| <p class="text-center mt-6 mb-0"> | ||
| {{ isSignup ? 'Already have an account?' : "Don't have an account?" }} | ||
| <button class="toggle-btn" type="button" :disabled="loading" @click="toggleMode"> | ||
| {{ isSignup ? 'Login' : 'Sign up' }} | ||
| </button> | ||
| </p> | ||
| </v-card> | ||
| </v-col> | ||
| </v-row> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script> | ||
| import Toolbar from '@/components/general/Toolbar.vue' | ||
| import { signInWithEmail, signInWithGoogle, signUpWithEmail } from '@/services/firebaseAuth' | ||
|
|
||
| export default { | ||
| components: { | ||
| Toolbar, | ||
| }, | ||
| data() { | ||
| return { | ||
| email: '', | ||
| password: '', | ||
| isSignup: false, | ||
| loading: false, | ||
| errorMessage: '', | ||
| } | ||
| }, | ||
| methods: { | ||
| toggleMode() { | ||
| this.isSignup = !this.isSignup | ||
| this.errorMessage = '' | ||
| }, | ||
| validateForm() { | ||
| if (!this.email || !this.password) { | ||
| this.errorMessage = 'Please enter both email and password.' | ||
| return false | ||
| } | ||
|
|
||
| if (this.password.length < 6) { | ||
| this.errorMessage = 'Password must be at least 6 characters long.' | ||
| return false | ||
| } | ||
|
Comment on lines
+107
to
+116
|
||
|
|
||
| return true | ||
| }, | ||
| async submitAuth() { | ||
| this.errorMessage = '' | ||
|
|
||
| if (!this.validateForm()) { | ||
| return | ||
| } | ||
|
|
||
| this.loading = true | ||
|
|
||
| try { | ||
| const normalizedEmail = this.email.trim() | ||
|
|
||
| if (this.isSignup) { | ||
| await signUpWithEmail(normalizedEmail, this.password) | ||
| } else { | ||
| await signInWithEmail(normalizedEmail, this.password) | ||
| } | ||
|
|
||
| this.$router.push('/dashboard') | ||
| } catch (error) { | ||
| this.errorMessage = error.message || 'Authentication failed. Please try again.' | ||
| } finally { | ||
| this.loading = false | ||
| } | ||
| }, | ||
| async loginWithGoogle() { | ||
| this.errorMessage = '' | ||
| this.loading = true | ||
|
|
||
| try { | ||
| await signInWithGoogle() | ||
| this.$router.push('/dashboard') | ||
| } catch (error) { | ||
| this.errorMessage = error.message || 'Google sign-in failed. Please try again.' | ||
| } finally { | ||
| this.loading = false | ||
| } | ||
| }, | ||
| }, | ||
| } | ||
| </script> | ||
|
|
||
| <style scoped> | ||
| .toggle-btn { | ||
| background: transparent; | ||
| border: 0; | ||
| color: #1976d2; | ||
| cursor: pointer; | ||
| font-weight: 600; | ||
| padding: 0; | ||
| } | ||
|
|
||
| .toggle-btn:disabled { | ||
| cursor: not-allowed; | ||
| opacity: 0.6; | ||
| } | ||
| </style> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the Auth route this button label changes to 'Home' but it still links to '/'. With the new guard redirecting unauthenticated '/' -> '/auth', clicking it from /auth will just navigate back to /auth (no-op). Either keep '/' publicly accessible, or change this button's target/visibility so it leads somewhere meaningful from /auth.