Hello from MCP server
<template>
<BaseLayoutNoAuth title="Login">
<!-- Loading Overlay -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-content">
<ion-spinner name="crescent" class="loading-spinner"></ion-spinner>
<p class="loading-message">Initializing your Price Builder...</p>
</div>
</div>
<div class="auth-container">
<div class="auth-card">
<!-- Logo/Brand -->
<div class="auth-header">
<img src="/images/branding-dark.png" alt="The New Flat Rate" class="auth-logo" />
<h1 class="auth-title">Welcome Back</h1>
<p class="auth-subtitle">Sign in to your account</p>
</div>
<!-- Error Alert -->
<ion-alert
:is-open="showError"
:header="errorTitle"
:message="errorMessage"
:buttons="['OK']"
@didDismiss="showError = false"
></ion-alert>
<!-- Form -->
<form @submit.prevent="onSubmit" class="auth-form">
<div class="form-group">
<label class="form-label">Email</label>
<div class="input-wrapper" :class="{ 'has-error': emailError && emailTouched }">
<ion-icon :icon="mailOutline" class="input-icon"></ion-icon>
<input
v-model="email"
type="email"
placeholder="you@example.com"
class="form-input"
@blur="emailTouched = true"
/>
</div>
<span v-if="emailError && emailTouched" class="error-text">{{ emailError }}</span>
</div>
<div class="form-group">
<label class="form-label">Password</label>
<div class="input-wrapper" :class="{ 'has-error': passwordError && passwordTouched }">
<ion-icon :icon="lockClosedOutline" class="input-icon"></ion-icon>
<input
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your password"
class="form-input"
@blur="passwordTouched = true"
/>
<ion-icon
:icon="showPassword ? eyeOffOutline : eyeOutline"
class="input-icon-right"
@click="showPassword = !showPassword"
></ion-icon>
</div>
<span v-if="passwordError && passwordTouched" class="error-text">{{ passwordError }}</span>
</div>
<button
type="submit"
class="submit-btn"
:disabled="!isFormValid"
>
Sign In
</button>
</form>
<!-- Footer Link -->
<div class="auth-footer">
<span class="footer-text">Don't have an account?</span>
<RouterLink to="/auth/register" class="footer-link">Create one</RouterLink>
</div>
</div>
</div>
</BaseLayoutNoAuth>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import router from "@/router";
import { IonAlert, IonIcon, IonSpinner } from "@ionic/vue";
import { mailOutline, lockClosedOutline, eyeOutline, eyeOffOutline } from "ionicons/icons";
import { getApi } from "@/dataAccess/getApi";
import { TypedPocketBase } from "@/pocketbase-types";
import BaseLayoutNoAuth from "@/components/BaseLayoutNoAuth.vue";
import { getSQLite, resetSQLiteConnection } from "@/dataAccess/getSQLite";
import { ChangesetProcessor, Change } from "@/dataAccess/changeset-processor";
import { syncDatabase } from "@/dataAccess/getDb";
import changesOne from "@/tnfrData/changesOne.json";
import changesTwo from "@/tnfrData/changesTwo.json";
import changesThree from "@/tnfrData/changesThree.json";
import changesFour from "@/tnfrData/changesFour.json";
import changesFive from "@/tnfrData/changesFive.json";
import changesSix from "@/tnfrData/changesSix.json";
import changesSeven from "@/tnfrData/changesSeven.json";
import changesEight from "@/tnfrData/changesEight.json";
import changesNine from "@/tnfrData/changesNine.json";
import changesTen from "@/tnfrData/changesTen.json";
import changesEleven from "@/tnfrData/changesEleven.json";
import changesTwelve from "@/tnfrData/changesTwelve.json";
const tnfrPlumbingChangesets = [
...changesOne,
...changesTwo,
...changesThree,
...changesFour,
...changesFive,
...changesSix,
...changesSeven,
...changesEight,
...changesNine,
...changesTen,
...changesEleven,
...changesTwelve,
];
const email = ref("");
const password = ref("");
const emailTouched = ref(false);
const passwordTouched = ref(false);
const showPassword = ref(false);
const showError = ref(false);
const errorTitle = ref("");
const errorMessage = ref("");
const isLoading = ref(false);
const emailError = computed(() => {
if (!email.value) return "";
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.value)) {
return "Please enter a valid email address";
}
return "";
});
const passwordError = computed(() => {
if (!password.value) return "";
if (password.value.length < 10) {
return "Password must be at least 10 characters";
}
return "";
});
const isFormValid = computed(() => {
return email.value.length > 0 &&
password.value.length >= 10 &&
!emailError.value &&
!passwordError.value;
});
let pb: TypedPocketBase;
async function resetDatabase(): Promise<ChangesetProcessor> {
console.log('[Login] Resetting database...');
await resetSQLiteConnection();
const sql = await getSQLite();
console.log('[Login] Database recreated with fresh migrations');
const now = new Date().toISOString();
await sql.dbConn.execute(`
INSERT INTO books (id, name, org, parent, refId, created, updated)
VALUES ('tnfrPlumbing', 'TNFR Plumbing', 'clientApp', NULL, 'tnfrPlumbing', '${now}', '${now}')
`, false);
await sql.saveDb();
console.log('[Login] tnfrPlumbing book record created');
const epochDate = new Date(0).toISOString();
const processor = new ChangesetProcessor(sql.dbConn);
for (const changesetObj of tnfrPlumbingChangesets) {
await processor.processChangeset(
changesetObj.changeset as Change[],
'clientApp',
true,
changesetObj.book,
epochDate
);
await sql.saveDb();
}
console.log('[Login] Database reset complete');
return processor;
}
async function onSubmit() {
if (!isFormValid.value) return;
isLoading.value = true;
try {
await pb
.collection("users")
.authWithPassword(email.value, password.value);
const processor = await resetDatabase();
console.log('[Login] Syncing changes from API...');
const syncResult = await syncDatabase();
console.log('[Login] Sync result:', syncResult);
// Flush accumulated processing logs now that DB is fully set up
await processor.flushLogs();
const returnTo = localStorage.getItem("relogin_returnTo");
localStorage.removeItem("relogin_returnTo");
localStorage.removeItem("relogin_email");
isLoading.value = false;
router.push(returnTo || "/directory");
} catch (error: any) {
isLoading.value = false;
console.error("Login failed:", error);
errorTitle.value = "Login Failed";
if (error.status === 400) {
if (error.message === "Failed to authenticate.") {
errorMessage.value = "The email or password you entered is incorrect. Please check your credentials and try again.";
} else if (error.data?.email?.message) {
if (error.data.email.message === "Missing required value.") {
errorMessage.value = "Please enter your email address.";
} else {
errorMessage.value = "Please enter a valid email address.";
}
} else if (error.data?.password?.message) {
if (error.data.password.message === "Missing required value.") {
errorMessage.value = "Please enter your password.";
} else {
errorMessage.value = "Please check your password and try again.";
}
} else {
errorMessage.value = "We couldn't log you in. Please check your credentials and try again.";
}
} else if (error.status === 403) {
errorMessage.value = "Login is currently unavailable. Please try again later or contact support.";
} else if (error.status === 404) {
errorMessage.value = "We're experiencing technical difficulties. Please try again later.";
} else if (error.status >= 500) {
errorMessage.value = "Our servers are experiencing issues. Please try again in a few moments.";
} else {
errorMessage.value = error.message || "An unexpected error occurred. Please try again.";
}
showError.value = true;
}
}
onMounted(async () => {
const api = await getApi(false);
pb = api.pb;
const reloginEmail = sessionStorage.getItem("relogin_email");
if (reloginEmail) {
email.value = reloginEmail;
}
});
</script>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--ion-background-color);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.loading-spinner {
width: 48px;
height: 48px;
color: var(--ion-color-primary);
}
.loading-message {
font-size: 18px;
font-weight: 500;
color: var(--ion-text-color);
margin: 0;
}
.auth-container {
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: linear-gradient(135deg, var(--ion-background-color) 0%, var(--ion-background-color-step-100) 100%);
}
.auth-card {
width: 100%;
max-width: 420px;
background: var(--ion-background-color);
border-radius: 16px;
padding: 40px 32px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}
.auth-header {
text-align: center;
margin-bottom: 32px;
}
.auth-logo {
height: 48px;
margin-bottom: 24px;
}
.auth-title {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 700;
color: var(--ion-text-color);
}
.auth-subtitle {
margin: 0;
font-size: 15px;
color: var(--ion-color-medium);
}
.auth-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 14px;
font-weight: 600;
color: var(--ion-text-color);
}
.input-wrapper {
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
height: 52px;
background: var(--ion-background-color-step-50);
border: 2px solid var(--ion-background-color-step-100);
border-radius: 10px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.input-wrapper:focus-within {
border-color: var(--ion-color-primary);
box-shadow: 0 0 0 3px rgba(var(--ion-color-primary-rgb), 0.15);
}
.input-wrapper.has-error {
border-color: var(--ion-color-danger);
}
.input-icon {
font-size: 20px;
color: var(--ion-color-medium);
flex-shrink: 0;
}
.input-icon-right {
font-size: 20px;
color: var(--ion-color-medium);
flex-shrink: 0;
cursor: pointer;
transition: color 0.2s ease;
}
.input-icon-right:hover {
color: var(--ion-text-color);
}
.form-input {
flex: 1;
border: none;
background: transparent;
font-size: 16px;
color: var(--ion-text-color);
outline: none;
}
.form-input::placeholder {
color: var(--ion-color-medium-shade);
}
.error-text {
font-size: 12px;
color: var(--ion-color-danger);
margin-top: 2px;
}
.submit-btn {
height: 52px;
margin-top: 8px;
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s ease, transform 0.1s ease;
}
.submit-btn:hover:not(:disabled) {
opacity: 0.9;
}
.submit-btn:active:not(:disabled) {
transform: scale(0.98);
}
.submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.auth-footer {
margin-top: 28px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.footer-text {
font-size: 14px;
color: var(--ion-color-medium);
}
.footer-link {
font-size: 14px;
font-weight: 600;
color: var(--ion-color-primary);
text-decoration: none;
transition: opacity 0.2s ease;
}
.footer-link:hover {
opacity: 0.8;
}
@media (max-width: 480px) {
.auth-card {
padding: 32px 24px;
}
.auth-title {
font-size: 24px;
}
}
</style>