feat: implement firebase authentication with terraform infrastructure
This commit is contained in:
+27
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Header from './components/layout/Header.vue'
|
||||
import Hero from './components/sections/Hero.vue'
|
||||
import About from './components/sections/About.vue'
|
||||
@@ -7,6 +7,9 @@ import History from './components/sections/History.vue'
|
||||
import Beer from './components/sections/Beer.vue'
|
||||
import HaumdaucherGame from './components/layout/HaumdaucherGame.vue'
|
||||
import { messages } from './locales/i18n'
|
||||
import { useAuth } from './composables/useAuth'
|
||||
|
||||
const { isAllowed } = useAuth()
|
||||
|
||||
const theme = ref('classic')
|
||||
const lang = ref<'de' | 'bar'>('de')
|
||||
@@ -95,6 +98,12 @@ const t = (key: string) => {
|
||||
:t="t"
|
||||
/>
|
||||
<main @click="triggerBSOD">
|
||||
|
||||
<!-- Member Banner -->
|
||||
<div v-if="isAllowed" class="member-banner">
|
||||
Do bist a haumdaucher 🫵 🍻
|
||||
</div>
|
||||
|
||||
<Hero :theme="theme" :t="t" />
|
||||
<About :t="t" />
|
||||
<History :t="t" />
|
||||
@@ -164,4 +173,21 @@ const t = (key: string) => {
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.member-banner {
|
||||
background: linear-gradient(90deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);
|
||||
color: #555;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
animation: slide-down 0.5s ease-out;
|
||||
margin-top: 60px; /* Offset for fixed header */
|
||||
border-bottom: 2px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
@keyframes slide-down {
|
||||
from { transform: translateY(-20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="gatekeeper">
|
||||
<div v-if="isLoading">
|
||||
<p class="animate-pulse">Loading permissions...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!user" class="text-center p-8 glass-panel rounded-xl">
|
||||
<h2 class="text-2xl font-bold mb-4">🔐 Locked Area</h2>
|
||||
<p class="mb-6">Sign in with your Google account to access this content.</p>
|
||||
<button @click="login" class="btn btn-primary">
|
||||
Sign in with Google
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isAllowed" class="text-center p-8 glass-panel rounded-xl border border-red-500/30">
|
||||
<h2 class="text-2xl font-bold mb-4 text-red-400">🚫 Access Pending</h2>
|
||||
<p class="mb-6">
|
||||
Hi <strong>{{ user.displayName }}</strong>. Your account ({{ user.email }}) is not yet on the guest list.
|
||||
</p>
|
||||
<p class="text-sm opacity-70">
|
||||
Please ask the admin to add "<strong>{{ user.email }}</strong>" to the allowlist.
|
||||
</p>
|
||||
<button @click="logout" class="btn btn-sm btn-ghost mt-4">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuth } from '../composables/useAuth'
|
||||
|
||||
const { user, isAllowed, isLoading, login, logout } = useAuth()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,9 @@ const props = defineProps<{
|
||||
const emit = defineEmits(['update:theme', 'update:lang', 'open:game'])
|
||||
|
||||
const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat']
|
||||
|
||||
import { useAuth } from '../../composables/useAuth'
|
||||
const { user, login, logout } = useAuth()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,8 +19,20 @@ const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat']
|
||||
<div class="container top-content">
|
||||
<div class="logo-text">HAUMDAUCHER</div>
|
||||
<div class="controls">
|
||||
<!-- Auth Control -->
|
||||
<div class="auth-control">
|
||||
<button v-if="!user" @click="login" class="login-btn">
|
||||
Login
|
||||
</button>
|
||||
<div v-else class="user-menu">
|
||||
<img :src="user.photoURL || ''" class="avatar" :title="user.displayName || ''" />
|
||||
<button @click="logout" class="logout-btn">Exit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Combined switch for better mobile spacing -->
|
||||
<div class="control-wrapper">
|
||||
|
||||
<div class="switch-group">
|
||||
<button
|
||||
v-for="l in ['de', 'bar']"
|
||||
@@ -213,4 +228,37 @@ button.active {
|
||||
font-weight: bold;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.auth-control {
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ref, watchEffect, computed } from 'vue'
|
||||
import { auth, db } from '../firebase'
|
||||
import { GoogleAuthProvider, signInWithPopup, signOut, type User } from 'firebase/auth'
|
||||
import { doc, onSnapshot } from 'firebase/firestore'
|
||||
|
||||
// Global state
|
||||
const user = ref<User | null>(null)
|
||||
const isAllowed = ref(false)
|
||||
const isLoading = ref(true)
|
||||
|
||||
// Watch auth state
|
||||
auth.onAuthStateChanged((u) => {
|
||||
user.value = u
|
||||
if (!u) {
|
||||
isAllowed.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Check allowlist
|
||||
watchEffect((onCleanup) => {
|
||||
if (!user.value) return
|
||||
|
||||
// Subscribe to the config/allowlist document
|
||||
const allowlistRef = doc(db, 'config', 'allowlist')
|
||||
const unsubscribe = onSnapshot(allowlistRef, (docSnap) => {
|
||||
isLoading.value = false
|
||||
if (docSnap.exists()) {
|
||||
const data = docSnap.data()
|
||||
const emails = data.emails || []
|
||||
isAllowed.value = emails.includes(user.value?.email)
|
||||
} else {
|
||||
isAllowed.value = false // Config doc missing -> deny all
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to check allowlist", err)
|
||||
// Don't set isAllowed to false here automatically, maybe retain retry logic? for now default to false
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
onCleanup(() => unsubscribe())
|
||||
})
|
||||
|
||||
export function useAuth() {
|
||||
const login = async () => {
|
||||
try {
|
||||
const provider = new GoogleAuthProvider()
|
||||
await signInWithPopup(auth, provider)
|
||||
} catch (e) {
|
||||
console.error("Login failed", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await signOut(auth)
|
||||
user.value = null
|
||||
isAllowed.value = false
|
||||
} catch (e) {
|
||||
console.error("Logout failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isAllowed: computed(() => isAllowed.value),
|
||||
isLoading: computed(() => isLoading.value),
|
||||
login,
|
||||
logout
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { getAuth } from 'firebase/auth'
|
||||
import { getFirestore } from 'firebase/firestore'
|
||||
|
||||
// Use environment variables or fallback for dev (though env variables are preferred)
|
||||
// In a real setup, these should be injected via VITE_FIREBASE_CONFIG or individual keys.
|
||||
const firebaseConfig = {
|
||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: import.meta.env.VITE_FIREBASE_APP_ID
|
||||
}
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig)
|
||||
|
||||
// Initialize services
|
||||
export const auth = getAuth(app)
|
||||
export const db = getFirestore(app)
|
||||
Reference in New Issue
Block a user