feat: implement firebase authentication with terraform infrastructure

This commit is contained in:
Moritz Graf
2026-01-02 21:56:41 +01:00
parent b4dcdc3667
commit dc733d8567
16 changed files with 1577 additions and 12 deletions
+27 -1
View File
@@ -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>
+46
View File
@@ -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>
+48
View File
@@ -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>
+72
View File
@@ -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
}
}
+21
View File
@@ -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)