Compare commits
No commits in common. "implement_firebase" and "master" have entirely different histories.
implement_
...
master
|
|
@ -10,17 +10,7 @@ lerna-debug.log*
|
|||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
# Local Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Terraform
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfvars
|
||||
*.tfplan
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
|
@ -32,6 +22,3 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Deprecated (Inlined in Terraform)
|
||||
firestore.rules
|
||||
|
|
|
|||
|
|
@ -3,15 +3,6 @@ FROM node:lts-alpine as build-stage
|
|||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Firebase Config Build Args
|
||||
ARG VITE_FIREBASE_API_KEY
|
||||
ARG VITE_FIREBASE_AUTH_DOMAIN
|
||||
ARG VITE_FIREBASE_PROJECT_ID
|
||||
ARG VITE_FIREBASE_STORAGE_BUCKET
|
||||
ARG VITE_FIREBASE_MESSAGING_SENDER_ID
|
||||
ARG VITE_FIREBASE_APP_ID
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
|
|
|
|||
11
GEMINI.md
11
GEMINI.md
|
|
@ -1,8 +1,5 @@
|
|||
# GEMINI.md - Haumdaucher Project Handbook
|
||||
|
||||
## 🚨 Rules
|
||||
**1. Infrastructure as Code is rule #1.** Manual creation of resources (e.g., via `gcloud` or Console) is forbidden. The use of Terraform/Tofu is mandatory.
|
||||
|
||||
This document serves as the "Source of Truth" for the Haumdaucher website. It outlines the design principles, technical architecture, and specialized features to ensure consistent future development.
|
||||
|
||||
## 🦢 Project Essence
|
||||
|
|
@ -28,14 +25,6 @@ This document serves as the "Source of Truth" for the Haumdaucher website. It ou
|
|||
- Managed via `vite-plugin-pwa`.
|
||||
- Custom icons and standalone manifest for "Add to Home Screen" support.
|
||||
|
||||
|
||||
## 🧪 Testing
|
||||
- **Framework**: Vitest + HappyDOM.
|
||||
- **Scope**: Lightweight sanity checks (e.g., verifying App mount).
|
||||
- **Commands**:
|
||||
- `npm test`: Run tests in watch mode.
|
||||
- `npm test -- --run`: Run tests once (CI mode).
|
||||
|
||||
## 🕹 The Haumdaucher Game
|
||||
- **Engine**: HTML5 Canvas rendering.
|
||||
- **Controls**: Touch-responsive (horizontal drag) and Keyboard (Arrow Keys).
|
||||
|
|
|
|||
70
README.md
70
README.md
|
|
@ -21,73 +21,3 @@ Use the provided deployment script to push to your Kubernetes cluster:
|
|||
./deploy.sh
|
||||
```
|
||||
Check [k8s-manifests.yaml](k8s-manifests.yaml) for resource definitions.
|
||||
|
||||
## 🚀 Bootstrap & Authentication (Infrastructure)
|
||||
|
||||
We use Terraform to manage Firebase Authentication and Firestore. To set this up for a new environment:
|
||||
|
||||
### 1. Manual Prerequisites (One-time)
|
||||
1. **Create a Project**: Go to [Google Cloud Console](https://console.cloud.google.com/) and create a new project.
|
||||
2. **Enable Billing**: Link a Billing Account to this project (Required for Terraform to enable Identity Platform).
|
||||
3. **Local Auth**:
|
||||
```bash
|
||||
gcloud auth login
|
||||
gcloud auth application-default login
|
||||
```
|
||||
4. **Configure OAuth (Crucial Step)**:
|
||||
* Go to [APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent).
|
||||
* Select **External** -> Create.
|
||||
* Fill in: App Name ("Haumdaucher"), Support Email, and Developer Contact Email. Click **Save and Continue** (You can skip scopes/test users for now).
|
||||
* Go to [Credentials](https://console.cloud.google.com/apis/credentials).
|
||||
* Click **+ Create Credentials** > **OAuth client ID**.
|
||||
* Type: **Web application**.
|
||||
* Name: "Haumdaucher Web".
|
||||
* **Authorized JavaScript origins**:
|
||||
* `http://localhost:5173`
|
||||
* `https://haumdaucher.de`
|
||||
* `https://haumdaucher.web.app` (Default Firebase Hosting URL)
|
||||
* **Authorized redirect URIs**:
|
||||
* `https://haumdaucher.firebaseapp.com/__/auth/handler`
|
||||
* Click **Create**.
|
||||
5. **Configure Secrets**:
|
||||
Run the helper script and paste the **Client ID** and **Client Secret** you just created:
|
||||
```bash
|
||||
./scripts/manage_secrets.py
|
||||
```
|
||||
6. **Verify**: Log in to the application. You should see the member banner.
|
||||
|
||||
## 🛠 Local Development Requirements
|
||||
|
||||
This project uses **Firebase** for authentication. To run the app locally, you need the environment variables (API keys) that are normally injected during deployment.
|
||||
|
||||
### 1. Generate Local Environment Config
|
||||
We typically manage infrastructure via Terraform. Use the included helper script to fetch the configuration from your active Terraform state and create a local `.env` file:
|
||||
|
||||
```bash
|
||||
npm run setup:env
|
||||
```
|
||||
|
||||
*Prerequisites: You must have `terraform` and `jq` installed, and `terraform apply` must have been run at least once.*
|
||||
|
||||
### 2. Start Dev Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 2. Infrastructure Deployment
|
||||
Navigate to the `terraform` directory and apply the configuration:
|
||||
|
||||
```bash
|
||||
cd terraform
|
||||
terraform init
|
||||
terraform apply -var="project_id=YOUR_PROJECT_ID" -var='allowed_users=["your_email@gmail.com"]'
|
||||
```
|
||||
|
||||
This will:
|
||||
- Enable Firebase, Firestore, and Identity APIs.
|
||||
- Create the Firestore Database.
|
||||
- Create the **Allowlist** in Firestore (`config/allowlist`).
|
||||
|
||||
### 3. Adding Friends
|
||||
To approve new users, simply update the `allowed_users` list in your `terraform.tfvars` (or CLI argument) and re-run `terraform apply`.
|
||||
|
||||
|
|
|
|||
25
deploy.sh
25
deploy.sh
|
|
@ -14,30 +14,7 @@ kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f
|
|||
|
||||
# Build the docker image
|
||||
echo "📦 Building Docker image..."
|
||||
|
||||
# Try to fetch Firebase config from Terraform
|
||||
if [ -d "terraform" ]; then
|
||||
echo "🔍 Detected Terraform directory. Fetching Firebase config..."
|
||||
cd terraform
|
||||
TF_OUT=$(terraform output -json firebase_config 2>/dev/null)
|
||||
cd ..
|
||||
|
||||
if [ ! -z "$TF_OUT" ]; then
|
||||
echo "✅ Firebase config found."
|
||||
FIREBASE_ARGS=(
|
||||
--build-arg VITE_FIREBASE_API_KEY=$(echo $TF_OUT | jq -r .apiKey)
|
||||
--build-arg VITE_FIREBASE_AUTH_DOMAIN=$(echo $TF_OUT | jq -r .authDomain)
|
||||
--build-arg VITE_FIREBASE_PROJECT_ID=$(echo $TF_OUT | jq -r .projectId)
|
||||
--build-arg VITE_FIREBASE_STORAGE_BUCKET=$(echo $TF_OUT | jq -r .storageBucket)
|
||||
--build-arg VITE_FIREBASE_MESSAGING_SENDER_ID=$(echo $TF_OUT | jq -r .messagingSenderId)
|
||||
--build-arg VITE_FIREBASE_APP_ID=$(echo $TF_OUT | jq -r .appId)
|
||||
)
|
||||
else
|
||||
echo "⚠️ Terraform output 'firebase_config' not found. Ensure required env vars are set."
|
||||
fi
|
||||
fi
|
||||
|
||||
docker build -t $IMAGE_NAME:$TAG "${FIREBASE_ARGS[@]}" .
|
||||
docker build -t $IMAGE_NAME:$TAG .
|
||||
|
||||
# Push the docker image
|
||||
echo "📤 Pushing Docker image to $REGISTRY..."
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>Haumdaucher Regensburg</title>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<link rel="apple-touch-icon" href="/icon-192.png" />
|
||||
</head>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
|
|
@ -5,26 +5,19 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"setup:env": "./scripts/setup-local-env.sh",
|
||||
"test": "vitest",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"firebase": "^12.7.0",
|
||||
"vue": "^3.3.4",
|
||||
"vuefire": "^3.2.2"
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.27",
|
||||
"@types/node": "^16.11.1",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.5.3",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-tsc": "^1.8.8"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,217 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
|
||||
# --- Configuration ---
|
||||
PROJECT_ID = "haumdaucher"
|
||||
SECRETS = {
|
||||
"haumdaucher-oauth-client-id": {
|
||||
"desc": "OAuth 2.0 Client ID",
|
||||
"instructions": (
|
||||
"1. Go to https://console.cloud.google.com/apis/credentials?project=haumdaucher\n"
|
||||
"2. Look for 'OAuth 2.0 Client IDs' -> 'Haumdaucher Web' (or create one if missing).\n"
|
||||
"3. Copy the 'Client ID' (looks like: 12345...apps.googleusercontent.com)"
|
||||
),
|
||||
"validation_hint": "Must end with .apps.googleusercontent.com"
|
||||
},
|
||||
"haumdaucher-oauth-client-secret": {
|
||||
"desc": "OAuth 2.0 Client Secret",
|
||||
"instructions": (
|
||||
"1. On the same credentials page, click the edit icon (pencil) or the name of the client.\n"
|
||||
"2. On the right side, find 'Client secret'.\n"
|
||||
"3. Copy the string (it is hidden by default)."
|
||||
),
|
||||
"validation_hint": "Usually a random string of characters."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Helpers ---
|
||||
class Colors:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
|
||||
def print_header(msg):
|
||||
print(f"\n{Colors.HEADER}{Colors.BOLD}=== {msg} ==={Colors.ENDC}")
|
||||
|
||||
def print_step(msg):
|
||||
print(f"\n{Colors.OKBLUE}👉 {msg}{Colors.ENDC}")
|
||||
|
||||
def print_success(msg):
|
||||
print(f"{Colors.OKGREEN}✅ {msg}{Colors.ENDC}")
|
||||
|
||||
def print_error(msg):
|
||||
print(f"{Colors.FAIL}❌ {msg}{Colors.ENDC}")
|
||||
|
||||
def run_command(command, check=True):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=check,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
if check:
|
||||
print_error(f"Command failed: {command}")
|
||||
print(e.stderr)
|
||||
sys.exit(1)
|
||||
return None
|
||||
|
||||
def verify_gcloud_login():
|
||||
"""Ensures user is logged in to gcloud."""
|
||||
account = run_command("gcloud config get-value account", check=False)
|
||||
if not account or account == "(unset)":
|
||||
print_error("You are not logged in to gcloud.")
|
||||
print(" Run: gcloud auth login")
|
||||
sys.exit(1)
|
||||
|
||||
project = run_command("gcloud config get-value project", check=False)
|
||||
if project != PROJECT_ID:
|
||||
print(f"{Colors.WARNING}⚠️ Current gcloud project is '{project}', but this script targets '{PROJECT_ID}'.{Colors.ENDC}")
|
||||
confirm = input(" Continue anyway? (y/n): ")
|
||||
if confirm.lower() != 'y':
|
||||
sys.exit(1)
|
||||
|
||||
def enable_api():
|
||||
"""Ensures Secret Manager API is enabled."""
|
||||
print("🔍 Checking API status...")
|
||||
run_command(f"gcloud services enable secretmanager.googleapis.com --project={PROJECT_ID}", check=False)
|
||||
|
||||
def get_secret_status(secret_id):
|
||||
"""Returns 'MISSING', 'EMPTY' (no versions), or 'READY'."""
|
||||
exists_cmd = f"gcloud secrets describe {secret_id} --project={PROJECT_ID} --format=json"
|
||||
meta = run_command(exists_cmd, check=False)
|
||||
|
||||
if not meta:
|
||||
return "MISSING"
|
||||
|
||||
# Check for versions
|
||||
versions_cmd = f"gcloud secrets versions list {secret_id} --project={PROJECT_ID} --limit=1 --filter='state=ENABLED' --format=json"
|
||||
versions = run_command(versions_cmd, check=False)
|
||||
|
||||
if not versions or versions == "[]":
|
||||
return "EMPTY"
|
||||
|
||||
return "READY"
|
||||
|
||||
def create_secret_resource(secret_id):
|
||||
print(f" creating secret container '{secret_id}'...")
|
||||
run_command(f"gcloud secrets create {secret_id} --replication-policy=automatic --project={PROJECT_ID}")
|
||||
|
||||
def add_secret_version(secret_name, value):
|
||||
process = subprocess.Popen(
|
||||
f"gcloud secrets versions add {secret_name} --data-file=- --project={PROJECT_ID}",
|
||||
shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
stdout, stderr = process.communicate(input=value)
|
||||
if process.returncode != 0:
|
||||
print_error(f"Failed to add version: {stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
def cleanup_old_versions(secret_name):
|
||||
cmd = f"gcloud secrets versions list {secret_name} --project={PROJECT_ID} --limit=10 --filter='state!=DESTROYED' --format=json"
|
||||
output = run_command(cmd, check=False)
|
||||
if not output:
|
||||
return
|
||||
|
||||
versions = json.loads(output)
|
||||
if len(versions) <= 1:
|
||||
return
|
||||
|
||||
# Keep latest enabled
|
||||
# The API returns sorted list usually, but let's be safe: assume index 0 is latest
|
||||
# Actually, we should just keep the one we just made.
|
||||
|
||||
print(f" 🧹 Cleaning up old versions for clean state...")
|
||||
# Skip the first one
|
||||
for v in versions[1:]:
|
||||
version_id = v['name'].split('/')[-1]
|
||||
run_command(f"gcloud secrets versions destroy {version_id} --secret={secret_name} --project={PROJECT_ID} --quiet", check=False)
|
||||
|
||||
|
||||
# --- Main Logic ---
|
||||
|
||||
def main():
|
||||
print_header("Haumdaucher Secret Manager Setup")
|
||||
verify_gcloud_login()
|
||||
# enable_api() # Managed via Terraform now
|
||||
|
||||
# Main Loop ensuring state
|
||||
while True:
|
||||
all_ready = True
|
||||
|
||||
# 1. Audit State
|
||||
print_header("Current Status Audit")
|
||||
status_map = {}
|
||||
for secret_id in SECRETS.keys():
|
||||
status = get_secret_status(secret_id)
|
||||
status_map[secret_id] = status
|
||||
|
||||
icon = "✅" if status == "READY" else "❌"
|
||||
msg = f"{secret_id}: {status}"
|
||||
if status == "READY":
|
||||
print(f"{Colors.OKGREEN}{icon} {msg}{Colors.ENDC}")
|
||||
else:
|
||||
print(f"{Colors.FAIL}{icon} {msg}{Colors.ENDC}")
|
||||
all_ready = False
|
||||
|
||||
if all_ready:
|
||||
print_success("All secrets are configured correctly!")
|
||||
break
|
||||
|
||||
# 2. Fix Missing Items
|
||||
print_header("Action Required")
|
||||
|
||||
for secret_id, config in SECRETS.items():
|
||||
status = status_map[secret_id]
|
||||
if status == "READY":
|
||||
continue
|
||||
|
||||
print_step(f"Configuring {config['desc']} ({secret_id})")
|
||||
|
||||
if status == "MISSING":
|
||||
create_secret_resource(secret_id)
|
||||
|
||||
print(f"{Colors.WARNING}INSTRUCTIONS:{Colors.ENDC}")
|
||||
print(config['instructions'])
|
||||
print("")
|
||||
|
||||
import getpass
|
||||
value = getpass.getpass(prompt=f"{Colors.BOLD}Paste {config['desc']} here (hidden): {Colors.ENDC}")
|
||||
|
||||
if not value.strip():
|
||||
print_error("Empty value provided. Retrying audit...")
|
||||
continue
|
||||
|
||||
add_secret_version(secret_id, value)
|
||||
cleanup_old_versions(secret_id)
|
||||
print_success("Secret updated.")
|
||||
|
||||
print("\n🔄 Re-verifying state...")
|
||||
# Loop continues to verify
|
||||
|
||||
print_header("Next Steps")
|
||||
print("1. Infrastructure is ready.")
|
||||
print("2. Run Terraform to apply the changes:")
|
||||
print(f"\n {Colors.OKBLUE}cd terraform && terraform apply{Colors.ENDC}\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ Aborted by user.")
|
||||
sys.exit(130)
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# setup-local-env.sh
|
||||
# Fetches Firebase configuration from Terraform outputs and creates a local .env file.
|
||||
|
||||
# Check if terraform is installed
|
||||
if ! command -v terraform &> /dev/null; then
|
||||
echo "❌ Error: terraform is not installed or not in PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "❌ Error: jq is not installed. Please install jq to parse JSON output."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TERRAFORM_DIR="terraform"
|
||||
ENV_FILE=".env"
|
||||
|
||||
echo "🔍 Fetching Firebase configuration from Terraform..."
|
||||
|
||||
if [ ! -d "$TERRAFORM_DIR" ]; then
|
||||
echo "❌ Error: Terraform directory '$TERRAFORM_DIR' not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$TERRAFORM_DIR" || exit
|
||||
|
||||
# Check if Terraform is initialized
|
||||
if [ ! -d ".terraform" ]; then
|
||||
echo "⚠️ Terraform not initialized. Initializing..."
|
||||
terraform init
|
||||
fi
|
||||
|
||||
# Fetch output in JSON format
|
||||
TF_OUT=$(terraform output -json firebase_config 2>/dev/null)
|
||||
cd ..
|
||||
|
||||
if [ -z "$TF_OUT" ]; then
|
||||
echo "❌ Error: Could not fetch 'firebase_config' from Terraform output."
|
||||
echo " Ensure 'terraform apply' has been run successfully."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📝 Writing to $ENV_FILE..."
|
||||
|
||||
# Parse JSON and write to .env
|
||||
# We use a heredoc to write the file content
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
VITE_FIREBASE_API_KEY=$(echo $TF_OUT | jq -r .apiKey)
|
||||
VITE_FIREBASE_AUTH_DOMAIN=$(echo $TF_OUT | jq -r .authDomain)
|
||||
VITE_FIREBASE_PROJECT_ID=$(echo $TF_OUT | jq -r .projectId)
|
||||
VITE_FIREBASE_STORAGE_BUCKET=$(echo $TF_OUT | jq -r .storageBucket)
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=$(echo $TF_OUT | jq -r .messagingSenderId)
|
||||
VITE_FIREBASE_APP_ID=$(echo $TF_OUT | jq -r .appId)
|
||||
EOF
|
||||
|
||||
echo "✅ Success! .env file created."
|
||||
cat "$ENV_FILE"
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from './App.vue'
|
||||
|
||||
describe('Smoke Test', () => {
|
||||
it('mounts properly', () => {
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders header text', () => {
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.text()).toContain('HAUMDAUCHER')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import Header from './components/layout/Header.vue'
|
||||
import Hero from './components/sections/Hero.vue'
|
||||
import About from './components/sections/About.vue'
|
||||
|
|
@ -7,9 +7,6 @@ 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')
|
||||
|
|
@ -98,9 +95,6 @@ const t = (key: string) => {
|
|||
:t="t"
|
||||
/>
|
||||
<main @click="triggerBSOD">
|
||||
|
||||
<!-- Member Banner -->
|
||||
|
||||
<Hero :theme="theme" :t="t" />
|
||||
<About :t="t" />
|
||||
<History :t="t" />
|
||||
|
|
@ -170,5 +164,4 @@ const t = (key: string) => {
|
|||
z-index: 9999;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
<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,53 +9,15 @@ const props = defineProps<{
|
|||
const emit = defineEmits(['update:theme', 'update:lang', 'open:game'])
|
||||
|
||||
const themes = ['classic', 'unicorn', 'luxury', 'win95', 'nat']
|
||||
|
||||
import { useAuth } from '../../composables/useAuth'
|
||||
import { watch, ref } from 'vue'
|
||||
const { user, login, logout, error } = useAuth()
|
||||
const showSettings = ref(false)
|
||||
|
||||
watch(user, (u) => {
|
||||
console.log("👤 Header: User changed:", u ? u.email : "null")
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<header class="fancy-glass header-top">
|
||||
<div class="container top-content">
|
||||
|
||||
<!-- Logo & Status Area -->
|
||||
<div class="branding-area">
|
||||
<div class="logo-text">HAUMDAUCHER</div>
|
||||
<div v-if="user" class="status-message">
|
||||
<span class="desktop-msg">{{ isNatUnlocked || isAllowed ? 'Du bist a Haumdaucher 🫵 🍻' : 'Vielleicht... bist du a Haumdaucher' }}</span>
|
||||
<span class="mobile-msg">{{ isNatUnlocked || isAllowed ? 'Haumdaucher! 🫵' : 'Vielleicht... 🤔' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
|
||||
<!-- Mobile Settings Toggle -->
|
||||
<button class="settings-toggle" @click="showSettings = !showSettings">
|
||||
⚙️
|
||||
</button>
|
||||
|
||||
<!-- Auth Control (Always Visible) -->
|
||||
<div class="auth-control">
|
||||
<div v-if="error" class="auth-error" :title="error">⚠️</div>
|
||||
<button v-if="!user" @click="login" class="login-btn">
|
||||
Login
|
||||
</button>
|
||||
<div v-else class="user-menu">
|
||||
<button @click="logout" class="logout-btn">Exit</button>
|
||||
<img :src="user.photoURL || ''" class="avatar" :title="user.displayName || ''" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Combined switch for better mobile spacing -->
|
||||
<div class="control-wrapper" :class="{ 'show-mobile': showSettings }">
|
||||
|
||||
<div class="control-wrapper">
|
||||
<div class="switch-group">
|
||||
<button
|
||||
v-for="l in ['de', 'bar']"
|
||||
|
|
@ -133,12 +95,6 @@ watch(user, (u) => {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Base Layout */
|
||||
.branding-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -154,58 +110,6 @@ watch(user, (u) => {
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
margin-left: 15px;
|
||||
color: #555;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-msg { display: none; }
|
||||
.settings-toggle { display: none; } /* Hidden on desktop */
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.branding-area {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
margin-left: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.desktop-msg { display: none; }
|
||||
.mobile-msg { display: inline; }
|
||||
|
||||
/* Settings Toggle Logic */
|
||||
.control-wrapper {
|
||||
display: none; /* Hidden by default on mobile */
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 10px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.control-wrapper.show-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.settings-toggle {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
padding: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-group {
|
||||
display: flex;
|
||||
background: rgba(0,0,0,0.05);
|
||||
|
|
@ -309,44 +213,4 @@ 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;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
color: #ff4444;
|
||||
margin-right: 8px;
|
||||
cursor: help;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
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)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Watch auth state
|
||||
auth.onAuthStateChanged((u) => {
|
||||
console.log("🔥 Auth State Changed:", u ? u.email : "Logged Out")
|
||||
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)
|
||||
error.value = "Failed to check allowlist: " + err.message
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
onCleanup(() => unsubscribe())
|
||||
})
|
||||
|
||||
export function useAuth() {
|
||||
const login = async () => {
|
||||
error.value = null
|
||||
try {
|
||||
const provider = new GoogleAuthProvider()
|
||||
await signInWithPopup(auth, provider)
|
||||
} catch (e: any) {
|
||||
console.error("Login failed", e)
|
||||
error.value = e.message
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
error.value = null
|
||||
try {
|
||||
await signOut(auth)
|
||||
user.value = null
|
||||
isAllowed.value = false
|
||||
} catch (e: any) {
|
||||
console.error("Logout failed", e)
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isAllowed: computed(() => isAllowed.value),
|
||||
isLoading: computed(() => isLoading.value),
|
||||
error: computed(() => error.value),
|
||||
login,
|
||||
logout
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Technical Details: Authentication & Infrastructure
|
||||
|
||||
This document explains the "Hybrid" approach we use for managing Authentication infrastructure on Google Cloud Platform (GCP).
|
||||
|
||||
## 🏗 The Architecture
|
||||
|
||||
We use **Terraform** to manage our "Infrastructure as Code". This gives us reproducibility and version control. However, Authentication involves sensitive credentials that have security restrictions preventing full automation.
|
||||
|
||||
### Components
|
||||
1. **Firebase Auth (Identity Platform)**: Handles user login (Google Sign-In).
|
||||
2. **Firestore**: database storing the "Allowlist" of approved emails.
|
||||
3. **Secret Manager**: securely stores the OAuth credentials used by Identity Platform.
|
||||
|
||||
## 🔐 The "Client Secret" Dilemma
|
||||
|
||||
### 1. Why is there a Manual Step?
|
||||
When you enable Firebase Authentication, Google creates an **OAuth 2.0 Client ID** for your web application. This Client ID comes with a **Client Secret**.
|
||||
|
||||
**The Restriction**: Google Cloud's security model **does not allow** retrieving an existing Client Secret via the API or CLI (`gcloud`). It is only visible:
|
||||
1. At the moment of creation.
|
||||
2. In the GCP Console UI (where you can "download JSON").
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **"Browser key" vs "OAuth Client"**:
|
||||
> You might see a "Browser key (auto created by Firebase)" in the console. This is an **API Key**, which is public and safe to share.
|
||||
> **However**, for the Identity Platform backend configuration (`google_identity_platform_default_supported_idp_config`), we strictly need the **OAuth 2.0 Client Secret**, which acts as a password for the server. Do not confuse the two!
|
||||
|
||||
Because Terraform interacts via the API, it **cannot** fetch this secret on its own. If we tried to create a *new* Client ID via Terraform, we would hit another blocker: the **OAuth Consent Screen** requires manual configuration in the Console.
|
||||
|
||||
### 2. The Solution: "Bootstrap" Script
|
||||
To bridge this gap, we use a "Bootstrap" workflow:
|
||||
|
||||
1. **Creation**: Firebase auto-creates the credentials.
|
||||
2. **Retrieval**: You manually copy them from the Console (the only place they are visible).
|
||||
3. **Storage**: You run `scripts/manage_secrets.py` to save them into **Google Secret Manager**.
|
||||
4. **Consumption**: Terraform reads the secure values from Secret Manager to configure the Identity Provider.
|
||||
|
||||
## 🔄 Authentication Flow
|
||||
|
||||
1. **User Visits Site**: The Vue.js app initializes Firebase using public config (API Key, Auth Domain).
|
||||
2. **Login**: User clicks "Login". The app invokes `signInWithPopup(googleProvider)`.
|
||||
* Google checks the allow-listed domains (`localhost`, `*.firebaseapp.com`).
|
||||
* The "Client ID" is used to identify the app to Google.
|
||||
3. **Token Exchange**: Identity Platform validates the user's Google credentials using the **Client Secret** (configured via Terraform).
|
||||
4. **Success**: If valid, the user is signed in.
|
||||
5. **Authorization**: The app watches `config/allowlist` in Firestore.
|
||||
* If `user.email` is in the list -> **Banner: "Do bist a haumdaucher"**.
|
||||
* If not -> Access is restricted.
|
||||
|
||||
## 📝 Terraform Resources involved
|
||||
* `google_identity_platform_default_supported_idp_config`: Configures the "Google" provider.
|
||||
* `data "google_secret_manager_secret_version"`: Fetches the stored credentials.
|
||||
* `google_firestore_document`: Manages the allowlist.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/google" {
|
||||
version = "5.45.2"
|
||||
constraints = "~> 5.0"
|
||||
hashes = [
|
||||
"h1:y2hf6zus1eKA5vpAfoyYkNBKDBQTVqmx4OVh3iRBaRo=",
|
||||
"zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a",
|
||||
"zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804",
|
||||
"zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e",
|
||||
"zh:3e555cf22a570a4bd247964671f421ed7517970cd9765ceb46f335edc2c6f392",
|
||||
"zh:688bd5b05a75124da7ae6e885b2b92bd29f4261808b2b78bd5f51f525c1052ca",
|
||||
"zh:6db3ef37a05010d82900bfffb3261c59a0c247e0692049cb3eb8c2ef16c9d7bf",
|
||||
"zh:70316fde75f6a15d72749f66d994ccbdde5f5ed4311b6d06b99850f698c9bbf9",
|
||||
"zh:84b8e583771a4f2bd514e519d98ed7fd28dce5efe0634e973170e1cfb5556fb4",
|
||||
"zh:9d4b8ef0a9b6677935c604d94495042e68ff5489932cfd1ec41052e094a279d3",
|
||||
"zh:a2089dd9bd825c107b148dd12d6b286f71aa37dfd4ca9c35157f2dcba7bc19d8",
|
||||
"zh:f03d795c0fd9721e59839255ee7ba7414173017dc530b4ce566daf3802a0d6dd",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/google-beta" {
|
||||
version = "5.45.2"
|
||||
constraints = "~> 5.0"
|
||||
hashes = [
|
||||
"h1:r9Tpv9w6j6hTI7MR7zeaUveGsyt/yNXjCmuO80asz98=",
|
||||
"zh:16b77bac5d1555b7f066ba8014f4fc8a6d0de64e252a1988d3fbb400984a4b19",
|
||||
"zh:1b13f515c4809343840aed8265915cc4191f138bdab5a8c5e1f542fdfc69989f",
|
||||
"zh:1dcce4309aeab7c88fd36aea664d57e620d8a413b967ce513a5a866e8de901f2",
|
||||
"zh:24db65d7929f2a731e9cac1750c569cb4528b312ef182a5e2e8c0cf008d8a71b",
|
||||
"zh:28c0b9e68d97570f03b2c4770607701580055bcba50069efd145954aa13b23e4",
|
||||
"zh:3a898a1ad1569f6486a2bc20014087284c8cab919bc8f155833de5128ccd12eb",
|
||||
"zh:4eed99cfb9daada70f813f2cedcf490d3097de1ccb9b391fc451ecc46509c067",
|
||||
"zh:888c4cb1f13b23674ba1091835dd3f1bff5d8e7729ef302183d8d01233819e54",
|
||||
"zh:8baae3b949f6e9505425f5fa4785de786e9cedc4c3f3ad906d8ed560bd2e39c6",
|
||||
"zh:cf2c8928b764592fa2cd14a9f109d01cd0a92049a4fca9d0a74cf2fe588364e2",
|
||||
"zh:edff09394f5bd0b278a4adc800a31b7f150249a1ea92ca273ccf4acd25be3f63",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
# Enable foundational APIs required for Terraform to manage other services
|
||||
resource "google_project_service" "cloudresourcemanager" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
service = "cloudresourcemanager.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "serviceusage" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
service = "serviceusage.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
# Enable required APIs
|
||||
resource "google_project_service" "firebase" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
service = "firebase.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
|
||||
depends_on = [
|
||||
google_project_service.cloudresourcemanager,
|
||||
google_project_service.serviceusage
|
||||
]
|
||||
}
|
||||
|
||||
resource "google_project_service" "identitytoolkit" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
service = "identitytoolkit.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "firestore" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
service = "firestore.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "firebaserules" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
service = "firebaserules.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
# Firebase Project
|
||||
resource "google_firebase_project" "default" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
|
||||
depends_on = [
|
||||
google_project_service.firebase,
|
||||
]
|
||||
}
|
||||
|
||||
# Firebase Web App
|
||||
resource "google_firebase_web_app" "default" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
display_name = "Haumdaucher Web"
|
||||
|
||||
depends_on = [google_firebase_project.default]
|
||||
}
|
||||
|
||||
data "google_firebase_web_app_config" "default" {
|
||||
provider = google-beta
|
||||
web_app_id = google_firebase_web_app.default.app_id
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
# Identity Platform (Auth)
|
||||
resource "google_identity_platform_config" "default" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
|
||||
# Authorized Domains for OAuth
|
||||
authorized_domains = [
|
||||
"localhost",
|
||||
"${var.project_id}.firebaseapp.com",
|
||||
"${var.project_id}.web.app",
|
||||
"haumdaucher.de",
|
||||
]
|
||||
|
||||
# Enable Google Sign-In (and others if needed, but keeping it simple)
|
||||
sign_in {
|
||||
allow_duplicate_emails = false
|
||||
|
||||
anonymous {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
email {
|
||||
enabled = false # We only want Google Sign-In
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [google_project_service.identitytoolkit]
|
||||
}
|
||||
|
||||
# Enable Google Default Identity Provider
|
||||
resource "google_identity_platform_default_supported_idp_config" "google" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
enabled = true
|
||||
idp_id = "google.com"
|
||||
client_id = data.google_secret_manager_secret_version.oauth_client_id.secret_data
|
||||
client_secret = data.google_secret_manager_secret_version.oauth_client_secret.secret_data
|
||||
|
||||
depends_on = [google_project_service.identitytoolkit]
|
||||
}
|
||||
|
||||
# NOTE: OAuth Client ID usually needs to be configured in console for Identity Platform
|
||||
# or imported. Terraform support for *creating* the OAuth client for IAP/Identity is limited/complex.
|
||||
# We will assume the default one created by Firebase is used or documented.
|
||||
|
||||
# Firestore Database (Native)
|
||||
resource "google_firestore_database" "database" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
name = "(default)"
|
||||
location_id = var.region
|
||||
type = "FIRESTORE_NATIVE"
|
||||
concurrency_mode = "OPTIMISTIC"
|
||||
app_engine_integration_mode = "DISABLED"
|
||||
|
||||
depends_on = [google_project_service.firestore]
|
||||
}
|
||||
|
||||
# Allowlist Configuration Document
|
||||
resource "google_firestore_document" "allowlist" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
database = google_firestore_database.database.name
|
||||
collection = "config"
|
||||
document_id = "allowlist"
|
||||
|
||||
# Serialize the list of emails into a JSON string map for the fields
|
||||
fields = jsonencode({
|
||||
emails = {
|
||||
arrayValue = {
|
||||
values = [
|
||||
for email in var.allowed_users : {
|
||||
stringValue = email
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
# Firestore Security Rules
|
||||
resource "google_firebaserules_ruleset" "firestore" {
|
||||
provider = google
|
||||
|
||||
source {
|
||||
files {
|
||||
name = "firestore.rules"
|
||||
content = <<-EOT
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
match /config/allowlist {
|
||||
allow read: if request.auth != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
google_project_service.firestore,
|
||||
google_project_service.firebaserules
|
||||
]
|
||||
}
|
||||
|
||||
resource "google_firebaserules_release" "firestore" {
|
||||
provider = google
|
||||
project = var.project_id
|
||||
name = "cloud.firestore" # This specific name targets the default Firestore database
|
||||
ruleset_name = google_firebaserules_ruleset.firestore.name
|
||||
|
||||
depends_on = [google_firebaserules_ruleset.firestore]
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
google-beta = {
|
||||
source = "hashicorp/google-beta"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "gcs" {
|
||||
bucket = "haumdaucher-tf-state"
|
||||
prefix = "terraform/state"
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
billing_project = var.project_id
|
||||
user_project_override = true
|
||||
}
|
||||
|
||||
provider "google-beta" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
billing_project = var.project_id
|
||||
user_project_override = true
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
output "firebase_config" {
|
||||
value = {
|
||||
apiKey = data.google_firebase_web_app_config.default.api_key
|
||||
authDomain = data.google_firebase_web_app_config.default.auth_domain
|
||||
projectId = var.project_id
|
||||
storageBucket = lookup(data.google_firebase_web_app_config.default, "storage_bucket", "${var.project_id}.appspot.com")
|
||||
messagingSenderId = data.google_firebase_web_app_config.default.messaging_sender_id
|
||||
appId = google_firebase_web_app.default.app_id
|
||||
}
|
||||
description = "Firebase Configuration Object for Frontend"
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# Fetch secrets from Secret Manager
|
||||
# Expects these secrets to be created via scripts/manage_secrets.py
|
||||
|
||||
data "google_secret_manager_secret_version" "oauth_client_id" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
secret = "haumdaucher-oauth-client-id"
|
||||
}
|
||||
|
||||
data "google_secret_manager_secret_version" "oauth_client_secret" {
|
||||
provider = google-beta
|
||||
project = var.project_id
|
||||
secret = "haumdaucher-oauth-client-secret"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
variable "project_id" {
|
||||
description = "The GCP Project ID"
|
||||
type = string
|
||||
default = "haumdaucher"
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "The GCP region for resources"
|
||||
type = string
|
||||
default = "europe-west3"
|
||||
}
|
||||
|
||||
|
||||
variable "allowed_users" {
|
||||
description = "List of email addresses allowed to access restricted features"
|
||||
type = list(string)
|
||||
default = ["moritz@haumdaucher.de"]
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
|
@ -37,9 +36,5 @@ export default defineConfig({
|
|||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
globals: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue