Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/general/InvisibleToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<v-app-bar color="transparent" flat dense>
<v-spacer />
<v-btn text dark class="mr-4" to="/login">
<v-btn text dark class="mr-4" to="/auth">
Login
<v-icon right>mdi-login</v-icon>
</v-btn>
Expand Down
2 changes: 1 addition & 1 deletion src/components/general/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</div>
<v-spacer />
<v-btn text dark to="/" v-if="$route.name != 'Dashboard'">
{{ $route.name != 'Login' ? 'Calibration' : 'Home' }}
{{ $route.name != 'Auth' ? 'Calibration' : 'Home' }}
</v-btn>
Comment on lines 7 to 9
Copy link

Copilot AI Mar 28, 2026

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.

Copilot uses AI. Check for mistakes.
<!-- <v-btn
text
Expand Down
42 changes: 35 additions & 7 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -18,6 +20,11 @@ const routes = [
name: 'LandingPage',
component: LandingPage,
},
{
path: '/auth',
name: 'Auth',
component: Auth,
},
// {
// path: '/login',
// name: 'Login',
Expand Down Expand Up @@ -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
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigation guard only runs for '/' and '/auth'. This means unauthenticated users can still directly access '/dashboard' (and other app routes) by entering the URL. Consider marking protected routes (e.g., /dashboard, /calibration/*) with meta.requiresAuth and enforcing auth in the guard, and decide whether '/' (LandingPage) should remain public or be turned into a redirect route instead of being blocked here.

Copilot uses AI. Check for mistakes.

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
74 changes: 74 additions & 0 deletions src/services/firebaseAuth.js
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) => {
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
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signInWithGoogle() awaits saveUserProfile(user). If the Firestore write fails (e.g., missing permissions), this will reject the whole sign-in flow even though the Firebase auth sign-in succeeded. Consider catching/logging profile-save errors and still returning the authenticated user, unless blocking is an explicit requirement.

Copilot uses AI. Check for mistakes.
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)
}
176 changes: 176 additions & 0 deletions src/views/Auth.vue
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
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The password field always uses autocomplete="current-password". In signup mode this should be new-password to avoid browser autofill issues and to better match password-manager expectations. Consider binding autocomplete based on isSignup.

Copilot uses AI. Check for mistakes.
/>

<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
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateForm() checks this.email directly, so whitespace-only input (e.g., ' ') passes validation but then trim() produces an empty email that will fail later with a Firebase error. Trim (and ideally normalize) the email during validation and validate against the trimmed value.

Copilot uses AI. Check for mistakes.

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>