mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
chore: initial commit - consolidate 4 projects into monorepo
Projects included: - maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing) - manacore (Expo mobile + SvelteKit web + Astro landing) - manadeck (NestJS backend + Expo mobile + SvelteKit web) - memoro (Expo mobile + SvelteKit web + Astro landing) This commit preserves the current state before monorepo restructuring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
e7f5f942f3
2450 changed files with 487047 additions and 0 deletions
57
manadeck/backend/.dockerignore
Normal file
57
manadeck/backend/.dockerignore
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment files - these should come from Cloud Run secrets
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.*.local
|
||||
env.example
|
||||
.env.example
|
||||
|
||||
# Test files
|
||||
*.spec.ts
|
||||
*.spec.js
|
||||
test
|
||||
coverage
|
||||
.nyc_output
|
||||
jest.config.js
|
||||
|
||||
# Development files
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
README.md
|
||||
README_*.md
|
||||
*.md
|
||||
DEPLOY_MANUAL.md
|
||||
verify-build.sh
|
||||
|
||||
# Build artifacts (optional - include if building inside Docker)
|
||||
dist
|
||||
|
||||
# IDE files
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.log
|
||||
|
||||
# Documentation
|
||||
docs
|
||||
|
||||
# GitHub workflows
|
||||
.github
|
||||
20
manadeck/backend/.env.example
Normal file
20
manadeck/backend/.env.example
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Server Configuration
|
||||
NODE_ENV=development
|
||||
PORT=8080
|
||||
|
||||
# Mana Core Configuration
|
||||
MANA_SERVICE_URL=https://mana-core-middleware-111768794939.europe-west3.run.app
|
||||
APP_ID=your-app-id-from-mana
|
||||
SERVICE_KEY=your-service-key-from-mana-core # REQUIRED for credit operations
|
||||
SIGNUP_REDIRECT_URL=https://manadeck.com/welcome
|
||||
|
||||
# Supabase Configuration (Your app's database)
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_SERVICE_KEY=your-service-key
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-jwt-secret
|
||||
|
||||
# CORS Configuration
|
||||
FRONTEND_URL=http://localhost:8081
|
||||
20
manadeck/backend/.env.production
Normal file
20
manadeck/backend/.env.production
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Server Configuration
|
||||
NODE_ENV=production
|
||||
PORT=8080
|
||||
|
||||
# Mana Core Configuration
|
||||
MANA_SERVICE_URL=https://mana-core-middleware-dev-55965480161.europe-west3.run.app
|
||||
APP_ID=cea4bfc6-a4de-4e17-91e2-54275940156e
|
||||
SERVICE_KEY=your-production-service-key
|
||||
SIGNUP_REDIRECT_URL=https://manadeck.com/welcome
|
||||
|
||||
# Supabase Configuration (Your app's database)
|
||||
SUPABASE_URL=https://vksoodohrbjwyloitvsz.supabase.co
|
||||
SUPABASE_ANON_KEY=sb_publishable_2ndX-kBHFpbDlL_ZeeOnfQ_ZlLI8ONk
|
||||
SUPABASE_SERVICE_KEY=your-production-supabase-service-key
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-production-jwt-secret
|
||||
|
||||
# CORS Configuration
|
||||
FRONTEND_URL=https://manadeck.com
|
||||
1
manadeck/backend/.gitignore
vendored
Normal file
1
manadeck/backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
4
manadeck/backend/.prettierrc
Normal file
4
manadeck/backend/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
487
manadeck/backend/DEPLOY_MANUAL.md
Normal file
487
manadeck/backend/DEPLOY_MANUAL.md
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
# Manadeck Backend Deployment Manual
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers deploying the Manadeck backend service to Google Cloud Run. The service can be deployed either:
|
||||
- **Automatically** via GitHub Actions (recommended)
|
||||
- **Manually** via Cloud Build and gcloud CLI
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Google Cloud SDK** installed and authenticated:
|
||||
```bash
|
||||
gcloud auth login
|
||||
gcloud config set project memo-2c4c4
|
||||
```
|
||||
|
||||
2. **Docker** installed (for local testing)
|
||||
|
||||
3. **Access to `memo-2c4c4` project** with:
|
||||
- Cloud Build API enabled
|
||||
- Cloud Run API enabled
|
||||
- Artifact Registry API enabled
|
||||
- Secret Manager API enabled
|
||||
|
||||
4. **Required permissions**:
|
||||
- Cloud Run Admin
|
||||
- Service Account User
|
||||
- Artifact Registry Writer
|
||||
- Secret Manager Secret Accessor
|
||||
|
||||
## Initial Setup (One-Time)
|
||||
|
||||
### 1. Create Artifact Registry Repository
|
||||
|
||||
```bash
|
||||
gcloud artifacts repositories create manadeck-backend \
|
||||
--repository-format=docker \
|
||||
--location=europe-west3 \
|
||||
--project=memo-2c4c4 \
|
||||
--description="Docker images for Manadeck Backend"
|
||||
```
|
||||
|
||||
### 2. Create Service Account
|
||||
|
||||
```bash
|
||||
# Create service account for Cloud Run
|
||||
gcloud iam service-accounts create manadeck-backend-sa \
|
||||
--display-name="Manadeck Backend Service Account" \
|
||||
--project=memo-2c4c4
|
||||
|
||||
# Get service account email
|
||||
SA_EMAIL="manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
|
||||
|
||||
# Grant necessary permissions
|
||||
gcloud projects add-iam-policy-binding memo-2c4c4 \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/run.admin"
|
||||
|
||||
gcloud projects add-iam-policy-binding memo-2c4c4 \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/iam.serviceAccountUser"
|
||||
|
||||
gcloud projects add-iam-policy-binding memo-2c4c4 \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/artifactregistry.writer"
|
||||
```
|
||||
|
||||
### 3. Create Secrets in Secret Manager
|
||||
|
||||
**IMPORTANT**: All secrets are stored in the `mana-core-453821` project (not `memo-2c4c4`).
|
||||
|
||||
```bash
|
||||
# Verify MANA_SERVICE_URL exists (global secret)
|
||||
gcloud secrets describe MANA_SERVICE_URL --project=mana-core-453821
|
||||
|
||||
# Create Manadeck-specific secrets in mana-core-453821
|
||||
echo "your-app-id" | gcloud secrets create MANADECK_APP_ID --data-file=- --project=mana-core-453821
|
||||
echo "your-service-key" | gcloud secrets create MANADECK_SERVICE_KEY --data-file=- --project=mana-core-453821
|
||||
echo "https://your-project.supabase.co" | gcloud secrets create MANADECK_SUPABASE_URL --data-file=- --project=mana-core-453821
|
||||
echo "your-supabase-anon-key" | gcloud secrets create MANADECK_SUPABASE_ANON_KEY --data-file=- --project=mana-core-453821
|
||||
echo "your-supabase-service-key" | gcloud secrets create MANADECK_SUPABASE_SERVICE_KEY --data-file=- --project=mana-core-453821
|
||||
echo "https://yourapp.com/welcome" | gcloud secrets create MANADECK_SIGNUP_REDIRECT_URL --data-file=- --project=mana-core-453821
|
||||
|
||||
# Grant service account (from memo-2c4c4) access to ALL secrets in mana-core-453821
|
||||
for SECRET in MANA_SERVICE_URL MANADECK_APP_ID MANADECK_SERVICE_KEY MANADECK_SUPABASE_URL MANADECK_SUPABASE_ANON_KEY MANADECK_SUPABASE_SERVICE_KEY MANADECK_SIGNUP_REDIRECT_URL; do
|
||||
gcloud secrets add-iam-policy-binding $SECRET \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--project=mana-core-453821
|
||||
done
|
||||
```
|
||||
|
||||
**Why mana-core-453821?** All Mana-related secrets are centralized in this project for easier management across multiple services.
|
||||
|
||||
### 4. Setup GitHub Secrets (for Automated Deployment)
|
||||
|
||||
Go to your repository → Settings → Secrets and variables → Actions
|
||||
|
||||
Add these secrets:
|
||||
- **GCP_SA_KEY_PROD**: Create and download a service account key:
|
||||
```bash
|
||||
gcloud iam service-accounts keys create key.json \
|
||||
--iam-account=manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com \
|
||||
--project=memo-2c4c4
|
||||
|
||||
# Copy contents of key.json to GitHub secret
|
||||
cat key.json
|
||||
```
|
||||
|
||||
- **CLOUD_RUN_SERVICE_ACCOUNT**: Set to `manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com`
|
||||
|
||||
## Automatic Deployment (GitHub Actions)
|
||||
|
||||
### Trigger Deployment
|
||||
|
||||
The GitHub Actions workflow automatically deploys when:
|
||||
- Code is pushed to `main` branch
|
||||
- Changes are made to `manadeck/backend/**` directory
|
||||
- Workflow file is modified
|
||||
|
||||
**Manual trigger:**
|
||||
1. Go to GitHub → Actions tab
|
||||
2. Select "Deploy Manadeck Backend to Cloud Run"
|
||||
3. Click "Run workflow"
|
||||
4. Choose environment and click "Run workflow"
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
1. ✅ **Test & Build Verification**
|
||||
- Runs linter
|
||||
- Type checks and builds
|
||||
- Runs tests
|
||||
|
||||
2. ✅ **Build & Deploy**
|
||||
- Builds Docker image
|
||||
- Pushes to Artifact Registry
|
||||
- Deploys to Cloud Run
|
||||
- Runs health checks
|
||||
|
||||
3. ✅ **Rollback** (if deployment fails)
|
||||
- Automatically rolls back to previous revision
|
||||
- Verifies rollback health
|
||||
|
||||
### Monitoring Deployment
|
||||
|
||||
- View progress in GitHub Actions tab
|
||||
- Check deployment summary in workflow run
|
||||
- View logs in Cloud Logging:
|
||||
```bash
|
||||
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
|
||||
--project=memo-2c4c4 \
|
||||
--limit=50
|
||||
```
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
### Option 1: Cloud Build (Build Only)
|
||||
|
||||
```bash
|
||||
# Navigate to backend directory
|
||||
cd manadeck/backend
|
||||
|
||||
# Submit build
|
||||
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
|
||||
```
|
||||
|
||||
This builds and pushes the Docker image to Artifact Registry but does NOT deploy to Cloud Run.
|
||||
|
||||
### Option 2: Full Manual Deployment
|
||||
|
||||
```bash
|
||||
# 1. Navigate to backend directory
|
||||
cd manadeck/backend
|
||||
|
||||
# 2. Update version in cloudbuild.yaml
|
||||
# Edit line 12 and 26: change v1.0.0 to v1.0.1 (or next version)
|
||||
|
||||
# 3. Build and push image
|
||||
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
|
||||
|
||||
# 4. Deploy to Cloud Run
|
||||
gcloud run deploy manadeck-backend \
|
||||
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0 \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3 \
|
||||
--platform=managed \
|
||||
--allow-unauthenticated \
|
||||
--min-instances=0 \
|
||||
--max-instances=10 \
|
||||
--memory=512Mi \
|
||||
--cpu=1 \
|
||||
--timeout=300 \
|
||||
--port=8080 \
|
||||
--service-account=manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com \
|
||||
--set-env-vars="NODE_ENV=production" \
|
||||
--update-secrets="MANA_SERVICE_URL=projects/mana-core-453821/secrets/MANA_SERVICE_URL:latest,APP_ID=projects/mana-core-453821/secrets/MANADECK_APP_ID:latest,SERVICE_KEY=projects/mana-core-453821/secrets/MANADECK_SERVICE_KEY:latest,SUPABASE_URL=projects/mana-core-453821/secrets/MANADECK_SUPABASE_URL:latest,SUPABASE_ANON_KEY=projects/mana-core-453821/secrets/MANADECK_SUPABASE_ANON_KEY:latest,SUPABASE_SERVICE_KEY=projects/mana-core-453821/secrets/MANADECK_SUPABASE_SERVICE_KEY:latest,SIGNUP_REDIRECT_URL=projects/mana-core-453821/secrets/MANADECK_SIGNUP_REDIRECT_URL:latest"
|
||||
```
|
||||
|
||||
## Testing Deployment
|
||||
|
||||
### Get Service URL
|
||||
|
||||
```bash
|
||||
SERVICE_URL=$(gcloud run services describe manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3 \
|
||||
--format='value(status.url)')
|
||||
|
||||
echo "Service URL: $SERVICE_URL"
|
||||
```
|
||||
|
||||
### Test Health Endpoints
|
||||
|
||||
```bash
|
||||
# Basic health check
|
||||
curl $SERVICE_URL/health
|
||||
|
||||
# Liveness check
|
||||
curl $SERVICE_URL/health/live
|
||||
|
||||
# Readiness check
|
||||
curl $SERVICE_URL/health/ready
|
||||
```
|
||||
|
||||
### Test Authenticated Endpoints
|
||||
|
||||
```bash
|
||||
# Example: Test with JWT token
|
||||
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
$SERVICE_URL/api/your-endpoint
|
||||
```
|
||||
|
||||
## Version Management
|
||||
|
||||
### Updating Version
|
||||
|
||||
To deploy a new version:
|
||||
|
||||
1. **Edit `cloudbuild.yaml`**:
|
||||
```yaml
|
||||
# Change v1.0.0 to v1.1.0
|
||||
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.1.0'
|
||||
```
|
||||
|
||||
2. **Build and deploy**:
|
||||
```bash
|
||||
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
|
||||
|
||||
gcloud run deploy manadeck-backend \
|
||||
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.1.0 \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
```
|
||||
|
||||
### List Deployed Revisions
|
||||
|
||||
```bash
|
||||
gcloud run revisions list \
|
||||
--service=manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
### Manual Rollback to Previous Revision
|
||||
|
||||
```bash
|
||||
# List revisions
|
||||
gcloud run revisions list \
|
||||
--service=manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
|
||||
# Rollback to specific revision
|
||||
gcloud run services update-traffic manadeck-backend \
|
||||
--to-revisions=manadeck-backend-00001-abc=100 \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
**Authentication errors:**
|
||||
```bash
|
||||
gcloud auth login
|
||||
gcloud auth configure-docker europe-west3-docker.pkg.dev
|
||||
```
|
||||
|
||||
**Permission denied:**
|
||||
- Verify you have Cloud Build Editor role
|
||||
- Check service account has Artifact Registry Writer role
|
||||
|
||||
### Deployment Fails
|
||||
|
||||
**Secret not found:**
|
||||
```bash
|
||||
# List secrets
|
||||
gcloud secrets list --project=memo-2c4c4
|
||||
|
||||
# Create missing secret
|
||||
echo "value" | gcloud secrets create SECRET_NAME --data-file=- --project=memo-2c4c4
|
||||
```
|
||||
|
||||
**Service account permissions:**
|
||||
```bash
|
||||
# Check service account IAM policy
|
||||
gcloud projects get-iam-policy memo-2c4c4 \
|
||||
--flatten="bindings[].members" \
|
||||
--filter="bindings.members:serviceAccount:manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com"
|
||||
```
|
||||
|
||||
### Health Check Fails
|
||||
|
||||
**Check logs:**
|
||||
```bash
|
||||
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
|
||||
--project=memo-2c4c4 \
|
||||
--limit=50 \
|
||||
--format=json
|
||||
```
|
||||
|
||||
**Common issues:**
|
||||
- Port mismatch (must be 8080)
|
||||
- Missing environment variables
|
||||
- Database connection issues
|
||||
- Mana Core service unreachable
|
||||
|
||||
**Test locally:**
|
||||
```bash
|
||||
cd manadeck/backend
|
||||
|
||||
# Build Docker image
|
||||
docker build -t manadeck-backend:local .
|
||||
|
||||
# Run container
|
||||
docker run -p 8080:8080 \
|
||||
-e NODE_ENV=production \
|
||||
-e MANA_SERVICE_URL=your-mana-url \
|
||||
-e APP_ID=your-app-id \
|
||||
manadeck-backend:local
|
||||
|
||||
# Test health
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### Update Environment Variables
|
||||
|
||||
```bash
|
||||
# Update a Manadeck-specific secret
|
||||
echo "new-value" | gcloud secrets versions add MANADECK_APP_ID --data-file=- --project=memo-2c4c4
|
||||
|
||||
# Redeploy to pick up new secret version
|
||||
gcloud run services update manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
|
||||
# Note: MANA_SERVICE_URL is a global secret - updating it affects all services
|
||||
```
|
||||
|
||||
### View Service Configuration
|
||||
|
||||
```bash
|
||||
gcloud run services describe manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3 \
|
||||
--format=yaml
|
||||
```
|
||||
|
||||
## Monitoring and Logs
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# Recent logs
|
||||
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
|
||||
--project=memo-2c4c4 \
|
||||
--limit=50
|
||||
|
||||
# Tail logs
|
||||
gcloud logging tail "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
|
||||
--project=memo-2c4c4
|
||||
|
||||
# Filter error logs
|
||||
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend AND severity>=ERROR" \
|
||||
--project=memo-2c4c4 \
|
||||
--limit=20
|
||||
```
|
||||
|
||||
### Cloud Console Links
|
||||
|
||||
- **Service**: https://console.cloud.google.com/run/detail/europe-west3/manadeck-backend/metrics?project=memo-2c4c4
|
||||
- **Logs**: https://console.cloud.google.com/logs/query?project=memo-2c4c4
|
||||
- **Artifact Registry**: https://console.cloud.google.com/artifacts/docker/memo-2c4c4/europe-west3/manadeck-backend?project=memo-2c4c4
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Environment Variables (via Secrets)
|
||||
|
||||
| Secret Name | Description | Example | Note |
|
||||
|------------|-------------|---------|------|
|
||||
| MANA_SERVICE_URL | Mana Core service URL | https://mana-core.example.com | Global secret |
|
||||
| MANADECK_APP_ID | Application ID from Mana | app-12345 | |
|
||||
| MANADECK_SERVICE_KEY | Service authentication key | sk_live_... | |
|
||||
| MANADECK_SUPABASE_URL | Supabase project URL | https://abc.supabase.co | |
|
||||
| MANADECK_SUPABASE_ANON_KEY | Supabase anonymous key | eyJhb... | |
|
||||
| MANADECK_SUPABASE_SERVICE_KEY | Supabase service role key | eyJhb... | |
|
||||
| MANADECK_SIGNUP_REDIRECT_URL | Post-signup redirect URL | https://app.example.com/welcome | |
|
||||
|
||||
### Cloud Run Configuration
|
||||
|
||||
- **Project**: memo-2c4c4
|
||||
- **Region**: europe-west3
|
||||
- **Service Name**: manadeck-backend
|
||||
- **Port**: 8080
|
||||
- **Memory**: 512Mi
|
||||
- **CPU**: 1
|
||||
- **Timeout**: 300s
|
||||
- **Min Instances**: 0 (scales to zero)
|
||||
- **Max Instances**: 10
|
||||
- **Concurrency**: 80
|
||||
|
||||
### Docker Image
|
||||
|
||||
- **Registry**: europe-west3-docker.pkg.dev
|
||||
- **Repository**: manadeck-backend
|
||||
- **Image**: europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend
|
||||
- **Tags**: v1.0.0, latest
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
```bash
|
||||
# Build only
|
||||
cd manadeck/backend
|
||||
gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
|
||||
|
||||
# Deploy latest version
|
||||
gcloud run deploy manadeck-backend \
|
||||
--image=europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
|
||||
# Get service URL
|
||||
gcloud run services describe manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3 \
|
||||
--format='value(status.url)'
|
||||
|
||||
# View logs
|
||||
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=manadeck-backend" \
|
||||
--project=memo-2c4c4 \
|
||||
--limit=20
|
||||
|
||||
# List revisions
|
||||
gcloud run revisions list \
|
||||
--service=manadeck-backend \
|
||||
--project=memo-2c4c4 \
|
||||
--region=europe-west3
|
||||
|
||||
# Update secret (example with Manadeck-specific secret)
|
||||
echo "new-value" | gcloud secrets versions add MANADECK_APP_ID \
|
||||
--data-file=- \
|
||||
--project=memo-2c4c4
|
||||
|
||||
# For global secrets like MANA_SERVICE_URL, update carefully as it affects all services
|
||||
echo "new-value" | gcloud secrets versions add MANA_SERVICE_URL \
|
||||
--data-file=- \
|
||||
--project=memo-2c4c4
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check Cloud Run logs for error messages
|
||||
2. Verify all secrets are configured correctly
|
||||
3. Test health endpoints
|
||||
4. Review GitHub Actions workflow logs (for automated deployments)
|
||||
5. Consult the troubleshooting section above
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-09-30
|
||||
**Current Version**: v1.0.0
|
||||
**Deployment Region**: europe-west3
|
||||
**Project**: memo-2c4c4
|
||||
110
manadeck/backend/Dockerfile
Normal file
110
manadeck/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache python3 make g++ git openssh-client
|
||||
|
||||
# Configure git to use HTTPS with token
|
||||
RUN git config --global url."https://github.com/".insteadOf "git@github.com:" && \
|
||||
git config --global url."https://".insteadOf "git://"
|
||||
|
||||
# Clone, build and package mana-core as a tarball
|
||||
RUN --mount=type=secret,id=github_token \
|
||||
if [ -f /run/secrets/github_token ]; then \
|
||||
export GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
|
||||
echo "Using GitHub token for private repo access" && \
|
||||
git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \
|
||||
else \
|
||||
echo "No GitHub token provided, attempting public clone" && \
|
||||
git clone https://github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \
|
||||
fi && \
|
||||
cd /tmp/mana-core && \
|
||||
npm install --force && \
|
||||
npm run build && \
|
||||
npm pack && \
|
||||
mv *.tgz /app/mana-core.tgz && \
|
||||
echo "Mana-core packaged as tarball at /app/mana-core.tgz"
|
||||
|
||||
# Copy package.json
|
||||
COPY package.json ./
|
||||
|
||||
# Replace GitHub URL with the tarball
|
||||
RUN sed -i 's|"git+https://github.com/Memo-2023/mana-core-nestjs-package.git"|"file:mana-core.tgz"|g' package.json || \
|
||||
sed -i 's|"github:Memo-2023/mana-core-nestjs-package"|"file:mana-core.tgz"|g' package.json
|
||||
|
||||
# Debug: Verify the replacement and file existence
|
||||
RUN echo "=== Verifying tarball and package.json ===" && \
|
||||
ls -la mana-core.tgz && \
|
||||
echo "Tarball exists at /app/mana-core.tgz" && \
|
||||
echo "Checking package.json replacement:" && \
|
||||
grep -n "mana-core" package.json && \
|
||||
echo "=== End verification ==="
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --legacy-peer-deps && \
|
||||
echo "Dependencies installed with mana-core from tarball"
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Debug: List the contents to verify build output
|
||||
RUN echo "=== Listing dist contents ===" && \
|
||||
ls -la dist/ || echo "No dist folder found" && \
|
||||
echo "=== Build complete ==="
|
||||
|
||||
# Production stage
|
||||
FROM node:18-alpine
|
||||
|
||||
# Install dumb-init for proper signal handling
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the dist folder
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Copy package.json for metadata
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
# Copy node_modules
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
# Debug in production to verify file structure
|
||||
RUN echo "=== Production stage file check ===" && \
|
||||
ls -la /app/ && \
|
||||
echo "=== Checking dist folder ===" && \
|
||||
ls -la /app/dist/ || echo "No dist folder" && \
|
||||
echo "=== Looking for main.js ===" && \
|
||||
find /app -name "main.js" -type f 2>/dev/null || echo "main.js not found"
|
||||
|
||||
# Change ownership to nodejs user
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER nodejs
|
||||
|
||||
# Expose the port (Cloud Run will set PORT env var)
|
||||
EXPOSE 8080
|
||||
|
||||
# Set environment to production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:' + (process.env.PORT || 8080) + '/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" || exit 1
|
||||
|
||||
# Use dumb-init to handle signals properly
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "dist/main"]
|
||||
98
manadeck/backend/README.md
Normal file
98
manadeck/backend/README.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
300
manadeck/backend/README_MANA_INTEGRATION.md
Normal file
300
manadeck/backend/README_MANA_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
# ManaDeck Backend with Mana Core Integration
|
||||
|
||||
## Overview
|
||||
|
||||
This is the NestJS backend for ManaDeck, integrated with the Mana Core authentication system. It provides a secure API for the ManaDeck mobile application with centralized authentication, user management, and database operations.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Mobile App (React Native/Expo)
|
||||
↓ API requests
|
||||
Backend Service (NestJS + @mana-core/nestjs-integration)
|
||||
↓ Proxy auth requests
|
||||
Mana Core Middleware (Central Auth Service)
|
||||
↓ User management
|
||||
Supabase (Database & Storage)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Mana Core authentication integration
|
||||
- ✅ Protected and public API endpoints
|
||||
- ✅ Supabase database integration with RLS
|
||||
- ✅ Health check endpoints
|
||||
- ✅ CORS configuration for mobile app
|
||||
- ✅ Docker containerization
|
||||
- ✅ Google Cloud Run deployment ready
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── config/
|
||||
│ │ └── validation.schema.ts # Environment variable validation
|
||||
│ ├── controllers/
|
||||
│ │ ├── api.controller.ts # Protected API endpoints
|
||||
│ │ ├── public.controller.ts # Public endpoints
|
||||
│ │ └── health.controller.ts # Health checks
|
||||
│ ├── services/
|
||||
│ │ └── supabase.service.ts # Database operations
|
||||
│ ├── app.controller.ts # Default app controller
|
||||
│ ├── app.module.ts # Main application module
|
||||
│ ├── app.service.ts # Default app service
|
||||
│ └── main.ts # Application entry point
|
||||
├── .env # Environment variables
|
||||
├── .env.example # Example environment file
|
||||
├── Dockerfile # Docker configuration
|
||||
├── cloudbuild.yaml # Google Cloud Build config
|
||||
└── package.json # Dependencies and scripts
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- NPM or Yarn
|
||||
- Mana Core Middleware running (locally or deployed)
|
||||
- Supabase project with configured database
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Copy environment variables
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```bash
|
||||
# Server Configuration
|
||||
NODE_ENV=development
|
||||
PORT=8080
|
||||
|
||||
# Mana Core Configuration
|
||||
MANA_SERVICE_URL=http://localhost:3000 # URL to Mana Core Middleware
|
||||
APP_ID=your-app-id # Your app's UUID
|
||||
SERVICE_KEY=your-service-key # Optional service key
|
||||
SIGNUP_REDIRECT_URL=https://yourapp.com/welcome
|
||||
|
||||
# Supabase Configuration
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_SERVICE_KEY=your-service-key # Optional for admin operations
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-jwt-secret
|
||||
|
||||
# CORS Configuration
|
||||
FRONTEND_URL=http://localhost:8081
|
||||
```
|
||||
|
||||
## Available Endpoints
|
||||
|
||||
### Authentication (Proxied by ManaCoreModule)
|
||||
- `POST /auth/signin` - Email/password sign-in
|
||||
- `POST /auth/signup` - User registration
|
||||
- `POST /auth/google-signin` - Google OAuth
|
||||
- `POST /auth/apple-signin` - Apple OAuth
|
||||
- `POST /auth/refresh` - Token refresh
|
||||
- `POST /auth/logout` - User logout
|
||||
- `POST /auth/forgot-password` - Password reset
|
||||
- `POST /auth/validate` - Token validation
|
||||
- `GET /auth/credits` - Get user credits
|
||||
- `GET /auth/devices` - Get user devices
|
||||
|
||||
### Protected API Endpoints (Requires Authentication)
|
||||
- `GET /v1/api/profile` - Get user profile
|
||||
- `GET /v1/api/decks` - Get user's decks
|
||||
- `POST /v1/api/decks` - Create a new deck
|
||||
- `PUT /v1/api/decks/:id` - Update a deck
|
||||
- `DELETE /v1/api/decks/:id` - Delete a deck
|
||||
- `GET /v1/api/cards` - Get user's cards
|
||||
- `POST /v1/api/cards` - Create a new card
|
||||
- `GET /v1/api/stats` - Get user statistics
|
||||
|
||||
### Public Endpoints
|
||||
- `GET /v1/public/featured-decks` - Get featured decks (personalized if authenticated)
|
||||
- `GET /v1/public/leaderboard` - Get leaderboard
|
||||
- `GET /v1/public/deck-templates` - Get deck templates
|
||||
- `GET /v1/public/announcements` - Get announcements
|
||||
- `GET /v1/public/health` - Basic health check
|
||||
- `GET /v1/public/version` - API version info
|
||||
|
||||
### Health Checks
|
||||
- `GET /health` - Comprehensive health check
|
||||
- `GET /health/ready` - Readiness probe
|
||||
- `GET /health/live` - Liveness probe
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Start development server with watch mode
|
||||
npm run start:dev
|
||||
|
||||
# Start production server
|
||||
npm run start:prod
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
|
||||
# Run linting
|
||||
npm run lint
|
||||
|
||||
# Build the application
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Testing the Integration
|
||||
|
||||
1. **Start Mana Core Middleware** (if running locally):
|
||||
```bash
|
||||
cd ../mana-core-middleware
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
2. **Start the Backend**:
|
||||
```bash
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
3. **Test Health Check**:
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
4. **Test Public Endpoint**:
|
||||
```bash
|
||||
curl http://localhost:8080/v1/public/version
|
||||
```
|
||||
|
||||
5. **Test Authentication**:
|
||||
```bash
|
||||
# Sign up
|
||||
curl -X POST http://localhost:8080/auth/signup \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"test@example.com","password":"password123"}'
|
||||
|
||||
# Sign in
|
||||
curl -X POST http://localhost:8080/auth/signin \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"test@example.com","password":"password123"}'
|
||||
```
|
||||
|
||||
6. **Test Protected Endpoint**:
|
||||
```bash
|
||||
# Use the token from signin response
|
||||
curl http://localhost:8080/v1/api/profile \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
### Build Docker Image
|
||||
```bash
|
||||
docker build -t manadeck-backend .
|
||||
```
|
||||
|
||||
### Run Docker Container
|
||||
```bash
|
||||
docker run -p 8080:8080 --env-file .env manadeck-backend
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Google Cloud Run
|
||||
|
||||
1. **Build and push to Container Registry**:
|
||||
```bash
|
||||
gcloud builds submit --config cloudbuild.yaml
|
||||
```
|
||||
|
||||
2. **Or deploy directly**:
|
||||
```bash
|
||||
gcloud run deploy manadeck-backend \
|
||||
--source . \
|
||||
--region us-central1 \
|
||||
--allow-unauthenticated
|
||||
```
|
||||
|
||||
## Mobile App Integration
|
||||
|
||||
Update your React Native app to use the backend:
|
||||
|
||||
1. **Update API configuration**:
|
||||
```javascript
|
||||
// utils/api.js
|
||||
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
```
|
||||
|
||||
2. **Use auth endpoints**:
|
||||
```javascript
|
||||
// Sign in
|
||||
const response = await fetch(`${API_URL}/auth/signin`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
```
|
||||
|
||||
3. **Use protected endpoints**:
|
||||
```javascript
|
||||
// Get profile
|
||||
const response = await fetch(`${API_URL}/v1/api/profile`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Always use HTTPS in production
|
||||
- Keep SERVICE_KEY secure and never expose it
|
||||
- Regularly rotate JWT_SECRET
|
||||
- Use environment-specific configurations
|
||||
- Enable rate limiting on auth endpoints
|
||||
- Implement proper CORS policies
|
||||
- Use Supabase RLS for data security
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "App verification failed"
|
||||
- Verify APP_ID matches the one registered with Mana Core
|
||||
- Check MANA_SERVICE_URL is correct and accessible
|
||||
|
||||
### "Token expired" errors
|
||||
- Ensure token refresh is implemented in the mobile app
|
||||
- Check token expiration settings in Mana Core
|
||||
|
||||
### "CORS errors" in mobile app
|
||||
- Update FRONTEND_URL in .env
|
||||
- Verify CORS configuration in main.ts
|
||||
|
||||
### "Cannot connect to Mana Core"
|
||||
- Ensure Mana Core Middleware is running
|
||||
- Check network connectivity
|
||||
- Verify MANA_SERVICE_URL is correct
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Database Setup**: Create tables in Supabase for decks, cards, etc.
|
||||
2. **RLS Policies**: Implement Row-Level Security in Supabase
|
||||
3. **Testing**: Add unit and integration tests
|
||||
4. **Monitoring**: Set up logging and monitoring
|
||||
5. **CI/CD**: Configure automated deployment pipeline
|
||||
6. **Documentation**: Generate API documentation with Swagger
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check the Mana Core documentation
|
||||
- Review the integration guides
|
||||
- Contact the development team
|
||||
176
manadeck/backend/SSH_LOCKFILE_SOLUTION.md
Normal file
176
manadeck/backend/SSH_LOCKFILE_SOLUTION.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# SSH URLs in package-lock.json - The Complete Solution
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Two-layer approach:**
|
||||
1. **CI test stage**: Patch lockfile SSH → HTTPS for `npm ci` (for tests)
|
||||
2. **Docker build**: Clone private repo, build tarball, replace with `file:` (for production image)
|
||||
|
||||
## The Problem
|
||||
|
||||
Your local machine converts HTTPS → SSH during `npm install`, baking SSH URLs into `package-lock.json`. CI/CD fails because it can't authenticate via SSH.
|
||||
|
||||
## Why Fighting It Locally Doesn't Work
|
||||
|
||||
❌ **Approach 1**: "Fix package.json to use HTTPS"
|
||||
- Doesn't work if git config rewrites it during install
|
||||
|
||||
❌ **Approach 2**: "Remove SSH rewrites from git config"
|
||||
- Inconvenient for developers
|
||||
- Easy to forget
|
||||
- Breaks other workflows
|
||||
|
||||
❌ **Approach 3**: "Temporarily disable git config during install"
|
||||
- Doesn't persist
|
||||
- Every developer needs to remember
|
||||
|
||||
## ✅ The Complete Solution (Two-Layer Approach)
|
||||
|
||||
**Accept that the lockfile has SSH URLs and handle them at two stages:**
|
||||
|
||||
### Layer 1: CI Test Stage (For Running Tests)
|
||||
|
||||
The GitHub Actions workflow uses the proven pattern to handle both SSH and HTTPS URLs:
|
||||
|
||||
```yaml
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false # Don't let default GITHUB_TOKEN interfere
|
||||
|
||||
- name: Configure git for private packages
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
git config --global url."https://${GH_TOKEN}@github.com/".insteadOf ssh://git@github.com/
|
||||
git config --global url."https://${GH_TOKEN}@github.com/".insteadOf git@github.com:
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Patch package-lock.json with authenticated URLs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
# Handle both SSH and HTTPS URLs
|
||||
if grep -q "git+ssh://git@github.com" package-lock.json; then
|
||||
echo "⚠️ SSH URLs found - patching to HTTPS with token..."
|
||||
sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
|
||||
echo "✓ Lockfile patched successfully"
|
||||
else
|
||||
echo "⚠️ HTTPS URLs found - injecting token..."
|
||||
sed -i "s|git+https://github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
|
||||
echo "✓ Token injected successfully"
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Layer 2: Docker Build (For Production Image)
|
||||
|
||||
The Dockerfile clones the private repo, builds it as a tarball, and installs from `file:`:
|
||||
|
||||
```dockerfile
|
||||
# Clone, build and package mana-core as a tarball
|
||||
RUN --mount=type=secret,id=github_token \
|
||||
if [ -f /run/secrets/github_token ]; then \
|
||||
export GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
|
||||
git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git /tmp/mana-core; \
|
||||
fi && \
|
||||
cd /tmp/mana-core && \
|
||||
npm install --force && \
|
||||
npm run build && \
|
||||
npm pack && \
|
||||
mv *.tgz /app/mana-core.tgz
|
||||
|
||||
# Copy package.json and replace GitHub URL with the tarball
|
||||
COPY package.json ./
|
||||
RUN sed -i 's|"git+https://github.com/Memo-2023/mana-core-nestjs-package.git"|"file:mana-core.tgz"|g' package.json
|
||||
|
||||
# Install dependencies from tarball
|
||||
RUN npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
The GitHub Actions workflow passes the token as a Docker secret:
|
||||
|
||||
```yaml
|
||||
- name: Build and Push Docker Image
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
run: |
|
||||
docker build \
|
||||
--secret id=github_token,env=GH_TOKEN \
|
||||
.
|
||||
```
|
||||
|
||||
## Why This Works
|
||||
|
||||
1. **Developers**: Use SSH locally (convenient, no config changes needed)
|
||||
2. **package-lock.json**: Contains SSH URLs (fine, we handle it in CI)
|
||||
3. **CI Test Stage**: Patches SSH → HTTPS for `npm ci` to run tests
|
||||
4. **Docker Build**: Clones repo, builds tarball, installs from `file:` (no git involved in final image)
|
||||
5. **Production**: Docker image has mana-core built into it, no runtime git dependency
|
||||
6. **Everyone's happy**: No git config changes, no local workflow disruption
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### For CI Test Stage (sed patching)
|
||||
|
||||
```bash
|
||||
sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json
|
||||
```
|
||||
|
||||
- Happens at runtime in CI (never committed)
|
||||
- Allows `npm ci` to install dependencies for testing
|
||||
- Works reliably every time
|
||||
|
||||
### For Docker Build (tarball approach)
|
||||
|
||||
```bash
|
||||
# Docker secret gives access to GitHub token
|
||||
--mount=type=secret,id=github_token
|
||||
|
||||
# Clone and build the private package
|
||||
git clone https://${GITHUB_TOKEN}@github.com/Memo-2023/mana-core-nestjs-package.git
|
||||
npm pack # Creates .tgz file
|
||||
|
||||
# Replace git URL with local file reference
|
||||
sed -i 's|"git+https://..."|"file:mana-core.tgz"|g' package.json
|
||||
```
|
||||
|
||||
- Private package is baked into the Docker image
|
||||
- No git dependency at runtime
|
||||
- Production image is fully self-contained
|
||||
|
||||
## Why This is Better Than Alternatives
|
||||
|
||||
| Approach | Developer Impact | Reliability | Production Quality | Maintenance |
|
||||
|----------|-----------------|-------------|-------------------|-------------|
|
||||
| Fix git config locally | 😡 High | 🔴 Low | ⚠️ Medium | 😱 High |
|
||||
| Require HTTPS in lockfile | 😡 High | 🔴 Low | ⚠️ Medium | 😱 High |
|
||||
| **Two-layer (sed + tarball)** | 😊 None | 🟢 100% | ✅ Excellent | 😌 None |
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **npm ci reads package-lock.json directly** - It doesn't care about git config
|
||||
2. **Fighting developer workflows is futile** - Accept SSH URLs locally
|
||||
3. **Two layers solve different problems**:
|
||||
- CI test stage needs quick install for testing → sed patch
|
||||
- Production image needs reliability and security → tarball bake-in
|
||||
4. **Docker secrets are the right tool** - Pass credentials without committing them
|
||||
5. **Self-contained images are better** - No runtime git dependencies
|
||||
|
||||
## References
|
||||
|
||||
This solution combines proven approaches:
|
||||
- **sed patching**: Used in `storyteller-project` for CI/CD
|
||||
- **tarball approach**: Used in memoro-service for production Docker images
|
||||
- Battle-tested across multiple projects
|
||||
|
||||
---
|
||||
|
||||
**Bottom line**: Commit the SSH lockfile, handle it in two layers (CI + Docker). Done. 🎯
|
||||
30
manadeck/backend/cloudbuild.yaml
Normal file
30
manadeck/backend/cloudbuild.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Cloud Build configuration for Manadeck Backend
|
||||
# Project: memo-2c4c4
|
||||
# Region: europe-west3
|
||||
# Manual deployment: gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml .
|
||||
|
||||
steps:
|
||||
# Build the Docker image
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- 'build'
|
||||
- '-t'
|
||||
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0'
|
||||
- '-t'
|
||||
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest'
|
||||
- '.'
|
||||
|
||||
# Push the Docker images to Artifact Registry
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args: ['push', 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0']
|
||||
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args: ['push', 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest']
|
||||
|
||||
# Specify the images to be stored in Artifact Registry
|
||||
images:
|
||||
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:v1.0.0'
|
||||
- 'europe-west3-docker.pkg.dev/memo-2c4c4/manadeck-backend/manadeck-backend:latest'
|
||||
|
||||
# Build timeout
|
||||
timeout: '1200s'
|
||||
257
manadeck/backend/create-secrets.sh
Executable file
257
manadeck/backend/create-secrets.sh
Executable file
|
|
@ -0,0 +1,257 @@
|
|||
#!/bin/bash
|
||||
# Create GCP secrets for Manadeck Backend
|
||||
# Usage: ./create-secrets.sh
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "=========================================="
|
||||
echo "Manadeck Backend - GCP Secrets Setup"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
DEPLOY_PROJECT_ID="memo-2c4c4" # Project where Cloud Run service is deployed
|
||||
SECRETS_PROJECT_ID="mana-core-453821" # Project where all secrets are stored
|
||||
REGION="europe-west3"
|
||||
|
||||
# Check if gcloud is installed
|
||||
if ! command -v gcloud &> /dev/null; then
|
||||
echo -e "${RED}✗${NC} gcloud CLI not found. Please install Google Cloud SDK."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓${NC} gcloud CLI found"
|
||||
echo ""
|
||||
|
||||
# Check authentication
|
||||
echo "Checking GCP authentication..."
|
||||
if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" &> /dev/null; then
|
||||
echo -e "${RED}✗${NC} Not authenticated with GCP. Running 'gcloud auth login'..."
|
||||
gcloud auth login
|
||||
fi
|
||||
|
||||
ACTIVE_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)")
|
||||
echo -e "${GREEN}✓${NC} Authenticated as: $ACTIVE_ACCOUNT"
|
||||
echo ""
|
||||
|
||||
# Set project for deployment
|
||||
echo "Deployment project: $DEPLOY_PROJECT_ID"
|
||||
echo "Secrets project: $SECRETS_PROJECT_ID"
|
||||
gcloud config set project $SECRETS_PROJECT_ID
|
||||
echo ""
|
||||
|
||||
# Prompt for secret values
|
||||
echo "=========================================="
|
||||
echo "Enter Secret Values"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: All secrets will be stored in project ${SECRETS_PROJECT_ID}${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if MANA_SERVICE_URL exists
|
||||
echo "Checking for global MANA_SERVICE_URL secret..."
|
||||
if gcloud secrets describe MANA_SERVICE_URL --project=$SECRETS_PROJECT_ID &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} MANA_SERVICE_URL secret exists"
|
||||
MANA_URL=$(gcloud secrets versions access latest --secret=MANA_SERVICE_URL --project=$SECRETS_PROJECT_ID 2>/dev/null || echo "")
|
||||
if [ -n "$MANA_URL" ]; then
|
||||
echo " Current value: $MANA_URL"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Could not read value (may need permissions)"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} MANA_SERVICE_URL secret not found!"
|
||||
read -p "Enter MANA_SERVICE_URL (e.g., https://mana-core.run.app): " MANA_URL
|
||||
|
||||
if [ -n "$MANA_URL" ]; then
|
||||
echo "Creating MANA_SERVICE_URL secret..."
|
||||
echo "$MANA_URL" | gcloud secrets create MANA_SERVICE_URL \
|
||||
--data-file=- \
|
||||
--project=$SECRETS_PROJECT_ID \
|
||||
--labels=service=global
|
||||
echo -e "${GREEN}✓${NC} MANA_SERVICE_URL secret created"
|
||||
else
|
||||
echo -e "${RED}✗${NC} MANA_SERVICE_URL is required"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Manadeck-specific secrets
|
||||
echo "Enter Manadeck-specific secrets:"
|
||||
echo ""
|
||||
|
||||
# APP_ID
|
||||
read -p "MANADECK_APP_ID (your app ID from Mana Core): " APP_ID
|
||||
if [ -z "$APP_ID" ]; then
|
||||
echo -e "${RED}✗${NC} APP_ID is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# SERVICE_KEY
|
||||
echo ""
|
||||
echo "SERVICE_KEY (for service-to-service authentication)"
|
||||
echo -e "${YELLOW}Press Enter to generate a random key, or paste your own:${NC}"
|
||||
read -p "" SERVICE_KEY
|
||||
if [ -z "$SERVICE_KEY" ]; then
|
||||
SERVICE_KEY=$(openssl rand -base64 32)
|
||||
echo -e "${GREEN}Generated SERVICE_KEY:${NC} $SERVICE_KEY"
|
||||
echo -e "${YELLOW}⚠ IMPORTANT: Add this to APP_SERVICE_KEYS in mana-core-middleware:${NC}"
|
||||
echo -e "${YELLOW} Format: ${APP_ID}:${SERVICE_KEY}${NC}"
|
||||
fi
|
||||
|
||||
# SUPABASE_URL
|
||||
echo ""
|
||||
read -p "MANADECK_SUPABASE_URL (e.g., https://xxx.supabase.co): " SUPABASE_URL
|
||||
if [ -z "$SUPABASE_URL" ]; then
|
||||
echo -e "${RED}✗${NC} SUPABASE_URL is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# SUPABASE_ANON_KEY
|
||||
echo ""
|
||||
read -p "MANADECK_SUPABASE_ANON_KEY: " SUPABASE_ANON_KEY
|
||||
if [ -z "$SUPABASE_ANON_KEY" ]; then
|
||||
echo -e "${RED}✗${NC} SUPABASE_ANON_KEY is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# SUPABASE_SERVICE_KEY
|
||||
echo ""
|
||||
read -p "MANADECK_SUPABASE_SERVICE_KEY (service role key): " SUPABASE_SERVICE_KEY
|
||||
if [ -z "$SUPABASE_SERVICE_KEY" ]; then
|
||||
echo -e "${RED}✗${NC} SUPABASE_SERVICE_KEY is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# SIGNUP_REDIRECT_URL
|
||||
echo ""
|
||||
read -p "MANADECK_SIGNUP_REDIRECT_URL (e.g., https://yourapp.com/welcome): " SIGNUP_REDIRECT_URL
|
||||
if [ -z "$SIGNUP_REDIRECT_URL" ]; then
|
||||
echo -e "${YELLOW}⚠${NC} SIGNUP_REDIRECT_URL is empty (optional)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Creating Secrets"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Function to create or update secret
|
||||
create_or_update_secret() {
|
||||
local SECRET_NAME=$1
|
||||
local SECRET_VALUE=$2
|
||||
|
||||
if [ -z "$SECRET_VALUE" ]; then
|
||||
echo -e "${YELLOW}⚠${NC} Skipping $SECRET_NAME (empty value)"
|
||||
return
|
||||
fi
|
||||
|
||||
if gcloud secrets describe $SECRET_NAME --project=$SECRETS_PROJECT_ID &> /dev/null; then
|
||||
echo -e "${YELLOW}⚠${NC} $SECRET_NAME already exists"
|
||||
read -p " Update with new value? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "$SECRET_VALUE" | gcloud secrets versions add $SECRET_NAME \
|
||||
--data-file=- \
|
||||
--project=$SECRETS_PROJECT_ID
|
||||
echo -e "${GREEN}✓${NC} $SECRET_NAME updated"
|
||||
else
|
||||
echo " Skipped $SECRET_NAME"
|
||||
fi
|
||||
else
|
||||
echo "Creating $SECRET_NAME..."
|
||||
echo "$SECRET_VALUE" | gcloud secrets create $SECRET_NAME \
|
||||
--data-file=- \
|
||||
--project=$SECRETS_PROJECT_ID \
|
||||
--labels=service=manadeck,environment=production
|
||||
echo -e "${GREEN}✓${NC} $SECRET_NAME created"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create all secrets
|
||||
create_or_update_secret "MANADECK_APP_ID" "$APP_ID"
|
||||
create_or_update_secret "MANADECK_SERVICE_KEY" "$SERVICE_KEY"
|
||||
create_or_update_secret "MANADECK_SUPABASE_URL" "$SUPABASE_URL"
|
||||
create_or_update_secret "MANADECK_SUPABASE_ANON_KEY" "$SUPABASE_ANON_KEY"
|
||||
create_or_update_secret "MANADECK_SUPABASE_SERVICE_KEY" "$SUPABASE_SERVICE_KEY"
|
||||
create_or_update_secret "MANADECK_SIGNUP_REDIRECT_URL" "$SIGNUP_REDIRECT_URL"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Grant Service Account Access"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
SERVICE_ACCOUNT="manadeck-backend-sa@${DEPLOY_PROJECT_ID}.iam.gserviceaccount.com"
|
||||
|
||||
echo "Checking if service account exists..."
|
||||
if gcloud iam service-accounts describe $SERVICE_ACCOUNT --project=$DEPLOY_PROJECT_ID &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Service account exists: $SERVICE_ACCOUNT"
|
||||
echo ""
|
||||
|
||||
read -p "Grant service account access to secrets? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
echo "Granting access to all secrets in ${SECRETS_PROJECT_ID}..."
|
||||
|
||||
# Grant access to all secrets
|
||||
for SECRET in MANA_SERVICE_URL MANADECK_APP_ID MANADECK_SERVICE_KEY MANADECK_SUPABASE_URL MANADECK_SUPABASE_ANON_KEY MANADECK_SUPABASE_SERVICE_KEY MANADECK_SIGNUP_REDIRECT_URL; do
|
||||
if gcloud secrets describe $SECRET --project=$SECRETS_PROJECT_ID &> /dev/null; then
|
||||
gcloud secrets add-iam-policy-binding $SECRET \
|
||||
--member="serviceAccount:${SERVICE_ACCOUNT}" \
|
||||
--role="roles/secretmanager.secretAccessor" \
|
||||
--project=$SECRETS_PROJECT_ID \
|
||||
--condition=None \
|
||||
2>/dev/null || echo " Access already granted for $SECRET"
|
||||
echo -e "${GREEN}✓${NC} $SECRET access granted"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Service account not found: $SERVICE_ACCOUNT"
|
||||
echo " Create it first with:"
|
||||
echo " gcloud iam service-accounts create manadeck-backend-sa \\"
|
||||
echo " --display-name=\"Manadeck Backend Service Account\" \\"
|
||||
echo " --project=$DEPLOY_PROJECT_ID"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Summary"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Secrets created/updated in project: $SECRETS_PROJECT_ID"
|
||||
echo ""
|
||||
echo "All secrets in mana-core-453821:"
|
||||
echo " - MANA_SERVICE_URL (global)"
|
||||
echo " - MANADECK_APP_ID"
|
||||
echo " - MANADECK_SERVICE_KEY"
|
||||
echo " - MANADECK_SUPABASE_URL"
|
||||
echo " - MANADECK_SUPABASE_ANON_KEY"
|
||||
echo " - MANADECK_SUPABASE_SERVICE_KEY"
|
||||
echo " - MANADECK_SIGNUP_REDIRECT_URL"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠ IMPORTANT NEXT STEPS:${NC}"
|
||||
echo ""
|
||||
echo "1. Add SERVICE_KEY to mana-core-middleware APP_SERVICE_KEYS:"
|
||||
echo " Format: ${APP_ID}:${SERVICE_KEY}"
|
||||
echo ""
|
||||
echo "2. Verify secrets:"
|
||||
echo " gcloud secrets list --project=$SECRETS_PROJECT_ID --filter=\"labels.service=manadeck\""
|
||||
echo ""
|
||||
echo "3. Deploy manadeck-backend:"
|
||||
echo " git add ."
|
||||
echo " git commit -m \"feat: configure secrets\""
|
||||
echo " git push origin main"
|
||||
echo ""
|
||||
echo "4. View secret values (if needed):"
|
||||
echo " gcloud secrets versions access latest --secret=MANADECK_APP_ID --project=$SECRETS_PROJECT_ID"
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Setup complete!"
|
||||
echo ""
|
||||
34
manadeck/backend/eslint.config.mjs
Normal file
34
manadeck/backend/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
);
|
||||
13
manadeck/backend/manadeck-sa-key.json
Normal file
13
manadeck/backend/manadeck-sa-key.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "memo-2c4c4",
|
||||
"private_key_id": "891033d18bb7a58f26590d7289aa10f171709511",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDlzSIuERbKHd4\nrTOgW3A3AiQ4oaluDWFrxXvMKOXeKsqMx2LZr3LjaG1VohR11/m3yNCrENwWwMil\nKlTzJridVZgxskpsnftj5LZiMIWTkGrbEKrR7t4EFGX7B1PlDByoHkg70UWSv4Cp\nLEqrKNc9GDa+HJuCpAu5Y8s5Vr1jcn4pNYcbe8Qz98M05u5SjsRi3Wo4sApQtMO+\n1I43+BBkQgP53rvbTGmvDkJPX+X30+pHrWIGq1RBbY99T39EfxgVfYbxKIkGTiAU\nT97I2L8A+AcTOZuYpeLVUYkpWyb/PhXRaBzZZpX4DuVl7rex3B903SqXEAmvXGUF\n08YtW3gHAgMBAAECggEAKHjMRRIkCtzXw9/LE/53vNozlohVH9Gi/AL0N+2qiws9\nuF21pae5VTcRkKumIjBbmtzx2MP3VrnzisG0rE+x2dSRVo82+TzDfQshdjXmHf+/\nLK9JOYF87UkvRP9iK74S2/LQLZiDFfeMbvUcSkh8KUcuZK5Uu3aoemcc8TNyovX8\na/rB1LqhMMh6e/5dm/na1iTAigonhlZl9xja4vinsa4k9wMwJrIW+eOpe5lA5Kd1\n9Y2iz4PaORNczUUeozJ820Y27S7WfvvbEGwXJgMO6fkkB+FGgVWl+BxY/BzATk6t\naeimdBWUNmRoqTAGND13hmVytEP0aBhM9bd/FSMOOQKBgQDgmAhyeWn6GkKMNDec\nB9bIWxW7P6ArdAHEnJQBWVStXYI01rS4T+Tet+D8w4Id4Z0d23UfVgMcF6E36xBG\nTpVYHlj30CfFXnkhiLxP2wx/i3ogNj8jrGU2yYKaE/72GzPx1YFHXXbTBdqrQRR+\nSY2ZgLyEpH0k6+H1bNIb7p1CswKBgQDe8OuUJHyQI2Elxv9eyE2TG73RA2TUCTH/\n9H7/ddtiYNFQv5m1u6a2tJomqFa9iTD0KPKJ1k43M63WjKRLlj0c1EFrVFEh7xuA\nYWtuuL9pl+LIskChI+HYBEn+4BejZs41kSDgOwQC+sztc52x77QbwB69XHvjTx8y\n+y49LUdPXQKBgH2JfW8/HlWFyBzGMy8r0oo7H3KCpeyj8zkKrE/0trgQxG70dB9x\nyOQUs9WVEm3RZhkCSf0x/pr9OkgfRAGIe0G99MzPtOtFWz+7Rkb8kYfZVnIa4D/4\nZLqy+CVXd/PoUKikRpa1T220n/2JawML6fIbW0RpDOpOTwb3xJmITq6xAoGAQnas\npMOj5wzFH5AN/eHVTrAMjM1oN9Zt85KeceZe+xtGC76zQnypHPsGodO52FOYJpI2\njGU5eDeN8dKIAIjPu1KEFk1Syr0QA9wHPUESv5YqZA7QOboK0G2m6bdBFNPHUizs\nEi1F6uV+RqQbNTrzBPrnryhtPr0N3hMIG/Dtu3kCgYEA1e2BtMIGnnM7Cm7fXfVp\nMeP5zBiqPcsUXVAoJ2WaYPj8n/BNhTd79STWZCUlTfcZ1IywIP9yMhleIXfKW35v\nNVFNMcduCXiqExWqmyjnVZJPFawO+KQ4G+JO8FzH6S/O82PUNbTRjJDs87cJs8t3\nSnd0TEt4+T9vAcdei3Kor20=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "manadeck-backend-sa@memo-2c4c4.iam.gserviceaccount.com",
|
||||
"client_id": "101484948835153964567",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/manadeck-backend-sa%40memo-2c4c4.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
8
manadeck/backend/nest-cli.json
Normal file
8
manadeck/backend/nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
81
manadeck/backend/package.json
Normal file
81
manadeck/backend/package.json
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mana-core/nestjs-integration": "git+https://github.com/Memo-2023/mana-core-nestjs-package.git",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"axios": "^1.7.2",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@supabase/supabase-js": "^2.58.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"joi": "^18.0.1",
|
||||
"nestjs-cls": "^6.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
173
manadeck/backend/setup-github-secrets.sh
Executable file
173
manadeck/backend/setup-github-secrets.sh
Executable file
|
|
@ -0,0 +1,173 @@
|
|||
#!/bin/bash
|
||||
# Script to generate values for GitHub Secrets
|
||||
# Run this script and copy the outputs to GitHub repository secrets
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "=========================================="
|
||||
echo "GitHub Secrets Setup for Manadeck Backend"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
PROJECT_ID="memo-2c4c4"
|
||||
SA_NAME="manadeck-backend-sa"
|
||||
SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
|
||||
|
||||
# Check if service account exists
|
||||
echo "Checking if service account exists..."
|
||||
if gcloud iam service-accounts describe $SA_EMAIL --project=$PROJECT_ID &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} Service account exists: $SA_EMAIL"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Service account not found. Creating..."
|
||||
|
||||
# Create service account
|
||||
gcloud iam service-accounts create $SA_NAME \
|
||||
--display-name="Manadeck Backend Service Account" \
|
||||
--project=$PROJECT_ID
|
||||
|
||||
echo -e "${GREEN}✓${NC} Service account created"
|
||||
|
||||
# Grant required roles
|
||||
echo "Granting required roles..."
|
||||
|
||||
gcloud projects add-iam-policy-binding $PROJECT_ID \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/run.admin" \
|
||||
--condition=None
|
||||
|
||||
gcloud projects add-iam-policy-binding $PROJECT_ID \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/iam.serviceAccountUser" \
|
||||
--condition=None
|
||||
|
||||
gcloud projects add-iam-policy-binding $PROJECT_ID \
|
||||
--member="serviceAccount:${SA_EMAIL}" \
|
||||
--role="roles/artifactregistry.writer" \
|
||||
--condition=None
|
||||
|
||||
echo -e "${GREEN}✓${NC} Roles granted"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GitHub Secret #1: CLOUD_RUN_SERVICE_ACCOUNT"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo -e "${BLUE}Copy this value:${NC}"
|
||||
echo ""
|
||||
echo "$SA_EMAIL"
|
||||
echo ""
|
||||
echo "Add to GitHub: Settings → Secrets → Actions → New repository secret"
|
||||
echo "Name: CLOUD_RUN_SERVICE_ACCOUNT"
|
||||
echo "Value: $SA_EMAIL"
|
||||
echo ""
|
||||
read -p "Press Enter when you've added CLOUD_RUN_SERVICE_ACCOUNT to GitHub..."
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GitHub Secret #2: GCP_SA_KEY_PROD"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check if key file already exists
|
||||
KEY_FILE="manadeck-sa-key.json"
|
||||
if [ -f "$KEY_FILE" ]; then
|
||||
echo -e "${YELLOW}⚠${NC} Key file already exists: $KEY_FILE"
|
||||
read -p "Do you want to create a new key? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Using existing key file..."
|
||||
else
|
||||
rm "$KEY_FILE"
|
||||
echo "Creating new service account key..."
|
||||
gcloud iam service-accounts keys create $KEY_FILE \
|
||||
--iam-account=$SA_EMAIL \
|
||||
--project=$PROJECT_ID
|
||||
echo -e "${GREEN}✓${NC} New key created"
|
||||
fi
|
||||
else
|
||||
echo "Creating service account key..."
|
||||
gcloud iam service-accounts keys create $KEY_FILE \
|
||||
--iam-account=$SA_EMAIL \
|
||||
--project=$PROJECT_ID
|
||||
echo -e "${GREEN}✓${NC} Key created"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}Copy the ENTIRE JSON content below:${NC}"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
cat $KEY_FILE
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Add to GitHub: Settings → Secrets → Actions → New repository secret"
|
||||
echo "Name: GCP_SA_KEY_PROD"
|
||||
echo "Value: [paste the entire JSON above]"
|
||||
echo ""
|
||||
read -p "Press Enter when you've added GCP_SA_KEY_PROD to GitHub..."
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "GitHub Secret #3: GH_PERSONAL_TOKEN"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo -e "${YELLOW}This token is needed to access private GitHub packages${NC}"
|
||||
echo ""
|
||||
echo "Steps to create:"
|
||||
echo "1. Go to: https://github.com/settings/tokens"
|
||||
echo "2. Click 'Generate new token (classic)'"
|
||||
echo "3. Name: 'Manadeck CI/CD'"
|
||||
echo "4. Select scope: 'repo' (Full control of private repositories)"
|
||||
echo "5. Click 'Generate token'"
|
||||
echo "6. Copy the token"
|
||||
echo ""
|
||||
echo "Add to GitHub: Settings → Secrets → Actions → New repository secret"
|
||||
echo "Name: GH_PERSONAL_TOKEN"
|
||||
echo "Value: [paste your GitHub token]"
|
||||
echo ""
|
||||
read -p "Press Enter when you've added GH_PERSONAL_TOKEN to GitHub..."
|
||||
|
||||
# Cleanup
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Cleanup"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠${NC} Security best practice: Delete the local key file"
|
||||
echo "The key file contains sensitive credentials and should not be kept locally."
|
||||
echo ""
|
||||
read -p "Delete $KEY_FILE now? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
rm -f $KEY_FILE
|
||||
echo -e "${GREEN}✓${NC} Key file deleted"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Key file kept: $KEY_FILE"
|
||||
echo " Remember to delete it manually when done!"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✅ Setup Complete!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Summary of GitHub Secrets:"
|
||||
echo "1. ✓ CLOUD_RUN_SERVICE_ACCOUNT = $SA_EMAIL"
|
||||
echo "2. ✓ GCP_SA_KEY_PROD = [JSON key from service account]"
|
||||
echo "3. ✓ GH_PERSONAL_TOKEN = [Your GitHub Personal Access Token]"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Verify all 3 secrets are in GitHub: https://github.com/Memo-2023/manadeck/settings/secrets/actions"
|
||||
echo "2. Run: ./create-secrets.sh (to set up GCP secrets)"
|
||||
echo "3. Push to main branch to trigger deployment"
|
||||
echo ""
|
||||
echo "Documentation:"
|
||||
echo "- DEPLOYMENT_CHECKLIST.md - Complete setup guide"
|
||||
echo "- DEPLOY_MANUAL.md - Detailed deployment docs"
|
||||
echo ""
|
||||
22
manadeck/backend/src/app.controller.spec.ts
Normal file
22
manadeck/backend/src/app.controller.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
12
manadeck/backend/src/app.controller.ts
Normal file
12
manadeck/backend/src/app.controller.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
62
manadeck/backend/src/app.module.ts
Normal file
62
manadeck/backend/src/app.module.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ManaCoreModule } from '@mana-core/nestjs-integration';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ApiController } from './controllers/api.controller';
|
||||
import { PublicController } from './controllers/public.controller';
|
||||
import { HealthController } from './controllers/health.controller';
|
||||
import { SupabaseService } from './services/supabase.service';
|
||||
import { validationSchema } from './config/validation.schema';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Context storage for request-scoped data (must be first)
|
||||
ClsModule.forRoot({
|
||||
global: true,
|
||||
middleware: {
|
||||
mount: true,
|
||||
generateId: true,
|
||||
},
|
||||
}),
|
||||
|
||||
// Configuration management
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
validationSchema,
|
||||
ignoreEnvFile: process.env.NODE_ENV === 'production',
|
||||
}),
|
||||
|
||||
// Mana Core authentication
|
||||
ManaCoreModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
manaServiceUrl: configService.get<string>('MANA_SERVICE_URL', 'https://mana-core-middleware-111768794939.europe-west3.run.app'),
|
||||
appId: configService.get<string>('APP_ID', 'cea4bfc6-a4de-4e17-91e2-54275940156e'),
|
||||
serviceKey: configService.get<string>('MANA_SUPABASE_SECRET_KEY', ''), // Required for service-to-service communication
|
||||
signupRedirectUrl: configService.get<string>('SIGNUP_REDIRECT_URL', 'https://manadeck.com/welcome'),
|
||||
debug: configService.get('NODE_ENV') === 'development',
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}) as any,
|
||||
|
||||
// Health checks
|
||||
TerminusModule,
|
||||
HttpModule,
|
||||
],
|
||||
controllers: [
|
||||
AppController,
|
||||
ApiController,
|
||||
PublicController,
|
||||
HealthController,
|
||||
],
|
||||
providers: [AppService, SupabaseService],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// Add any custom middleware here
|
||||
}
|
||||
}
|
||||
8
manadeck/backend/src/app.service.ts
Normal file
8
manadeck/backend/src/app.service.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
51
manadeck/backend/src/config/credit-operations.ts
Normal file
51
manadeck/backend/src/config/credit-operations.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Credit Operation Types and Costs for Manadeck
|
||||
*
|
||||
* Define all billable operations and their credit costs here.
|
||||
* This ensures consistent pricing across the application.
|
||||
*/
|
||||
|
||||
export enum CreditOperationType {
|
||||
DECK_CREATION = 'deck_creation',
|
||||
CARD_CREATION = 'card_creation',
|
||||
AI_CARD_GENERATION = 'ai_card_generation',
|
||||
AI_DECK_GENERATION = 'ai_deck_generation',
|
||||
DECK_EXPORT = 'deck_export',
|
||||
// Add more operation types as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit costs for each operation type
|
||||
*/
|
||||
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
|
||||
[CreditOperationType.DECK_CREATION]: 10, // 10 mana to create a deck
|
||||
[CreditOperationType.CARD_CREATION]: 2, // 2 mana to create a card
|
||||
[CreditOperationType.AI_CARD_GENERATION]: 5, // 5 mana for AI-generated card
|
||||
[CreditOperationType.AI_DECK_GENERATION]: 30, // 30 mana for AI-generated deck
|
||||
[CreditOperationType.DECK_EXPORT]: 3, // 3 mana to export deck
|
||||
};
|
||||
|
||||
/**
|
||||
* Operation descriptions for user-facing messages
|
||||
*/
|
||||
export const OPERATION_DESCRIPTIONS: Record<CreditOperationType, string> = {
|
||||
[CreditOperationType.DECK_CREATION]: 'Create a new deck',
|
||||
[CreditOperationType.CARD_CREATION]: 'Add a card to your deck',
|
||||
[CreditOperationType.AI_CARD_GENERATION]: 'Generate card with AI',
|
||||
[CreditOperationType.AI_DECK_GENERATION]: 'Generate deck with AI',
|
||||
[CreditOperationType.DECK_EXPORT]: 'Export deck',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get credit cost for an operation
|
||||
*/
|
||||
export function getCreditCost(operationType: CreditOperationType): number {
|
||||
return CREDIT_COSTS[operationType] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operation description
|
||||
*/
|
||||
export function getOperationDescription(operationType: CreditOperationType): string {
|
||||
return OPERATION_DESCRIPTIONS[operationType] || operationType;
|
||||
}
|
||||
25
manadeck/backend/src/config/validation.schema.ts
Normal file
25
manadeck/backend/src/config/validation.schema.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import * as Joi from 'joi';
|
||||
|
||||
export const validationSchema = Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test')
|
||||
.default('development'),
|
||||
PORT: Joi.number().default(8080),
|
||||
|
||||
// Mana Core Required
|
||||
MANA_SERVICE_URL: Joi.string().required(),
|
||||
APP_ID: Joi.string().uuid().required(),
|
||||
MANA_SUPABASE_SECRET_KEY: Joi.string().optional(),
|
||||
SIGNUP_REDIRECT_URL: Joi.string().uri().optional(),
|
||||
|
||||
// Your app's database
|
||||
SUPABASE_URL: Joi.string().uri().required(),
|
||||
SUPABASE_ANON_KEY: Joi.string().required(),
|
||||
SUPABASE_SERVICE_KEY: Joi.string().required(), // Required for edge functions
|
||||
|
||||
// JWT
|
||||
JWT_SECRET: Joi.string().optional(),
|
||||
|
||||
// CORS
|
||||
FRONTEND_URL: Joi.string().uri().optional(),
|
||||
});
|
||||
318
manadeck/backend/src/controllers/api.controller.ts
Normal file
318
manadeck/backend/src/controllers/api.controller.ts
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Logger, BadRequestException, Req } from '@nestjs/common';
|
||||
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
|
||||
import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
|
||||
import { CreditClientService } from '@mana-core/nestjs-integration';
|
||||
import { CreditOperationType, getCreditCost, getOperationDescription } from '../config/credit-operations';
|
||||
import { SupabaseService } from '../services/supabase.service';
|
||||
|
||||
@Controller('api')
|
||||
@UseGuards(AuthGuard)
|
||||
export class ApiController {
|
||||
private readonly logger = new Logger(ApiController.name);
|
||||
|
||||
constructor(
|
||||
private readonly creditClient: CreditClientService,
|
||||
private readonly supabaseService: SupabaseService,
|
||||
) {}
|
||||
|
||||
@Get('profile')
|
||||
async getProfile(@CurrentUser() user: any) {
|
||||
this.logger.log(`Getting profile for user: ${user.sub}`);
|
||||
|
||||
// Include credit balance in profile
|
||||
let creditBalance = 0;
|
||||
try {
|
||||
const balance = await this.creditClient.getCreditBalance(user.sub);
|
||||
creditBalance = balance.balance || 0;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to fetch credit balance for user ${user.sub}:`, error);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
credits: creditBalance,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('credits/balance')
|
||||
async getCreditBalance(@CurrentUser() user: any) {
|
||||
this.logger.log(`Getting credit balance for user: ${user.sub}`);
|
||||
|
||||
try {
|
||||
const balance = await this.creditClient.getCreditBalance(user.sub);
|
||||
|
||||
return {
|
||||
userId: user.sub,
|
||||
balance: balance.balance || 0,
|
||||
currency: 'mana',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching credit balance for user ${user.sub}:`, error);
|
||||
throw new BadRequestException({
|
||||
error: 'credit_balance_fetch_failed',
|
||||
message: 'Failed to retrieve credit balance',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Get('decks')
|
||||
getUserDecks(@CurrentUser() user: any) {
|
||||
// This would fetch from Supabase in a real implementation
|
||||
return {
|
||||
userId: user.sub,
|
||||
decks: [],
|
||||
message: 'Fetch user decks from Supabase',
|
||||
};
|
||||
}
|
||||
|
||||
@Post('decks')
|
||||
async createDeck(@CurrentUser() user: any, @Body() deckData: any) {
|
||||
this.logger.log(`Creating deck for user: ${user.sub}`);
|
||||
|
||||
const operationType = CreditOperationType.DECK_CREATION;
|
||||
const creditCost = getCreditCost(operationType);
|
||||
|
||||
try {
|
||||
// 1. Pre-flight credit validation
|
||||
const validation = await this.creditClient.validateCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
);
|
||||
|
||||
if (!validation.hasCredits) {
|
||||
this.logger.warn(
|
||||
`User ${user.sub} has insufficient credits. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
);
|
||||
|
||||
throw new BadRequestException({
|
||||
error: 'insufficient_credits',
|
||||
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
requiredCredits: creditCost,
|
||||
availableCredits: validation.availableCredits,
|
||||
operation: getOperationDescription(operationType),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Perform the operation (create deck in Supabase)
|
||||
// TODO: Implement actual deck creation logic with Supabase
|
||||
const newDeck = {
|
||||
id: `deck_${Date.now()}`,
|
||||
...deckData,
|
||||
userId: user.sub,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
// 3. Success - Consume credits
|
||||
await this.creditClient.consumeCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
`Created deck: ${deckData.name || 'Unnamed Deck'}`,
|
||||
{
|
||||
deckId: newDeck.id,
|
||||
deckName: deckData.name,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(`Deck created successfully for user ${user.sub}. ${creditCost} credits consumed.`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: user.sub,
|
||||
deck: newDeck,
|
||||
creditsUsed: creditCost,
|
||||
message: 'Deck created successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's already a BadRequestException (insufficient credits), rethrow it
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log other errors
|
||||
this.logger.error(`Error creating deck for user ${user.sub}:`, error);
|
||||
throw new BadRequestException({
|
||||
error: 'deck_creation_failed',
|
||||
message: error.message || 'Failed to create deck',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Post('decks/generate')
|
||||
async generateDeckWithAI(@CurrentUser() user: any, @Body() requestData: any, @Req() req: any) {
|
||||
this.logger.log(`Generating AI deck for user: ${user.sub}`);
|
||||
|
||||
const { prompt, deckTitle, deckDescription, cardCount = 10, cardTypes, difficulty, tags } = requestData;
|
||||
|
||||
// Validate required fields
|
||||
if (!prompt || !deckTitle) {
|
||||
throw new BadRequestException({
|
||||
error: 'validation_failed',
|
||||
message: 'prompt and deckTitle are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (cardCount < 1 || cardCount > 50) {
|
||||
throw new BadRequestException({
|
||||
error: 'validation_failed',
|
||||
message: 'cardCount must be between 1 and 50',
|
||||
});
|
||||
}
|
||||
|
||||
const operationType = CreditOperationType.AI_DECK_GENERATION;
|
||||
const creditCost = getCreditCost(operationType);
|
||||
|
||||
try {
|
||||
// 1. Pre-flight credit validation
|
||||
const validation = await this.creditClient.validateCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
);
|
||||
|
||||
if (!validation.hasCredits) {
|
||||
this.logger.warn(
|
||||
`User ${user.sub} has insufficient credits for AI deck generation. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
);
|
||||
|
||||
throw new BadRequestException({
|
||||
error: 'insufficient_credits',
|
||||
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
requiredCredits: creditCost,
|
||||
availableCredits: validation.availableCredits,
|
||||
operation: getOperationDescription(operationType),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Get the Mana token from the request
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
throw new BadRequestException({
|
||||
error: 'authentication_failed',
|
||||
message: 'No authorization token found',
|
||||
});
|
||||
}
|
||||
const manaToken = authHeader.replace('Bearer ', '');
|
||||
|
||||
// 3. Call the edge function via Supabase
|
||||
const result = await this.supabaseService.generateDeckWithAI(
|
||||
user.sub,
|
||||
{
|
||||
prompt,
|
||||
deckTitle,
|
||||
deckDescription,
|
||||
cardCount,
|
||||
cardTypes,
|
||||
difficulty,
|
||||
tags,
|
||||
},
|
||||
manaToken,
|
||||
);
|
||||
|
||||
// 4. Check if the edge function was successful
|
||||
if (!result.success) {
|
||||
throw new BadRequestException({
|
||||
error: 'deck_generation_failed',
|
||||
message: result.error || 'Failed to generate deck',
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Success - Consume credits
|
||||
await this.creditClient.consumeCredits(
|
||||
user.sub,
|
||||
operationType,
|
||||
creditCost,
|
||||
`Generated AI deck: ${deckTitle}`,
|
||||
{
|
||||
deckId: result.deck?.id,
|
||||
deckTitle,
|
||||
cardCount: result.deck?.card_count,
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.log(`AI deck generated successfully for user ${user.sub}. ${creditCost} credits consumed.`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId: user.sub,
|
||||
deck: result.deck,
|
||||
cards: result.cards,
|
||||
creditsUsed: creditCost,
|
||||
message: result.message || 'Deck generated successfully with AI',
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's already a BadRequestException, rethrow it
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log other errors
|
||||
this.logger.error(`Error generating AI deck for user ${user.sub}:`, error);
|
||||
throw new BadRequestException({
|
||||
error: 'deck_generation_failed',
|
||||
message: error.message || 'Failed to generate deck with AI',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Put('decks/:id')
|
||||
updateDeck(
|
||||
@CurrentUser() user: any,
|
||||
@Param('id') deckId: string,
|
||||
@Body() deckData: any,
|
||||
) {
|
||||
this.logger.log(`Updating deck ${deckId} for user: ${user.sub}`);
|
||||
return {
|
||||
userId: user.sub,
|
||||
deckId,
|
||||
deck: deckData,
|
||||
message: 'Deck would be updated in Supabase',
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('decks/:id')
|
||||
deleteDeck(@CurrentUser() user: any, @Param('id') deckId: string) {
|
||||
this.logger.log(`Deleting deck ${deckId} for user: ${user.sub}`);
|
||||
return {
|
||||
userId: user.sub,
|
||||
deckId,
|
||||
message: 'Deck would be deleted from Supabase',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('cards')
|
||||
getUserCards(@CurrentUser() user: any) {
|
||||
return {
|
||||
userId: user.sub,
|
||||
cards: [],
|
||||
message: 'Fetch user cards from Supabase',
|
||||
};
|
||||
}
|
||||
|
||||
@Post('cards')
|
||||
createCard(@CurrentUser() user: any, @Body() cardData: any) {
|
||||
this.logger.log(`Creating card for user: ${user.sub}`);
|
||||
return {
|
||||
userId: user.sub,
|
||||
card: cardData,
|
||||
message: 'Card would be created in Supabase',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
getUserStats(@CurrentUser() user: any) {
|
||||
return {
|
||||
userId: user.sub,
|
||||
stats: {
|
||||
totalDecks: 0,
|
||||
totalCards: 0,
|
||||
lastActive: new Date(),
|
||||
},
|
||||
message: 'Fetch user stats from Supabase',
|
||||
};
|
||||
}
|
||||
}
|
||||
55
manadeck/backend/src/controllers/health.controller.ts
Normal file
55
manadeck/backend/src/controllers/health.controller.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckService,
|
||||
HttpHealthIndicator,
|
||||
HealthCheck,
|
||||
} from '@nestjs/terminus';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Public } from '@mana-core/nestjs-integration/decorators';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private health: HealthCheckService,
|
||||
private http: HttpHealthIndicator,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
check() {
|
||||
const manaServiceUrl = this.configService.get<string>('MANA_SERVICE_URL')!;
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
|
||||
return this.health.check([
|
||||
() => this.http.pingCheck('mana-core', manaServiceUrl),
|
||||
() => this.http.pingCheck('supabase', `${supabaseUrl}/rest/v1/`),
|
||||
]);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('ready')
|
||||
@HealthCheck()
|
||||
readiness() {
|
||||
return this.health.check([
|
||||
() => ({
|
||||
ready: {
|
||||
status: 'up',
|
||||
message: 'Service is ready to receive requests',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('live')
|
||||
liveness() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date(),
|
||||
uptime: process.uptime(),
|
||||
environment: this.configService.get<string>('NODE_ENV'),
|
||||
};
|
||||
}
|
||||
}
|
||||
94
manadeck/backend/src/controllers/public.controller.ts
Normal file
94
manadeck/backend/src/controllers/public.controller.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Controller, Get, UseGuards, Query, Logger } from '@nestjs/common';
|
||||
import { OptionalAuthGuard } from '@mana-core/nestjs-integration/guards';
|
||||
import { CurrentUser, Public } from '@mana-core/nestjs-integration/decorators';
|
||||
|
||||
@Controller('public')
|
||||
export class PublicController {
|
||||
private readonly logger = new Logger(PublicController.name);
|
||||
|
||||
@Public()
|
||||
@Get('health')
|
||||
health() {
|
||||
return { status: 'ok', timestamp: new Date() };
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('version')
|
||||
version() {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
service: 'manadeck-backend',
|
||||
environment: process.env.NODE_ENV,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('featured-decks')
|
||||
getFeaturedDecks(@CurrentUser() user?: any) {
|
||||
// User might be authenticated or null
|
||||
if (user) {
|
||||
this.logger.log(`Getting personalized featured decks for user: ${user.sub}`);
|
||||
return {
|
||||
type: 'personalized',
|
||||
userId: user.sub,
|
||||
decks: [],
|
||||
message: 'Personalized featured decks based on user preferences',
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.log('Getting generic featured decks');
|
||||
return {
|
||||
type: 'generic',
|
||||
decks: [],
|
||||
message: 'Generic featured decks for anonymous users',
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('leaderboard')
|
||||
getLeaderboard(@CurrentUser() user?: any, @Query('limit') limit = '10') {
|
||||
const limitNum = parseInt(limit, 10);
|
||||
|
||||
if (user) {
|
||||
this.logger.log(`Getting leaderboard with user ${user.sub} position`);
|
||||
return {
|
||||
leaderboard: [],
|
||||
userPosition: null,
|
||||
userId: user.sub,
|
||||
limit: limitNum,
|
||||
message: 'Leaderboard with user position highlighted',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
leaderboard: [],
|
||||
limit: limitNum,
|
||||
message: 'Public leaderboard',
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('deck-templates')
|
||||
getDeckTemplates(@Query('category') category?: string) {
|
||||
return {
|
||||
category: category || 'all',
|
||||
templates: [],
|
||||
message: 'Public deck templates available for all users',
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('announcements')
|
||||
getAnnouncements() {
|
||||
return {
|
||||
announcements: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Welcome to ManaDeck!',
|
||||
content: 'Your deck management system is now powered by Mana Core authentication.',
|
||||
date: new Date(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
47
manadeck/backend/src/main.ts
Normal file
47
manadeck/backend/src/main.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
|
||||
logger.log('Starting ManaDeck Backend...');
|
||||
|
||||
// Debug: Log environment variables before validation
|
||||
logger.log('=== Environment Variables Debug ===');
|
||||
logger.log(`APP_ID: ${process.env.APP_ID ? process.env.APP_ID.substring(0, 20) + '...' : 'NOT SET'}`);
|
||||
logger.log(`SUPABASE_URL: ${process.env.SUPABASE_URL || 'NOT SET'}`);
|
||||
logger.log(`MANA_SERVICE_URL: ${process.env.MANA_SERVICE_URL || 'NOT SET'}`);
|
||||
logger.log('===================================');
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||||
});
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
|
||||
// Enable CORS
|
||||
app.enableCors({
|
||||
origin: configService.get('FRONTEND_URL') || true,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
});
|
||||
|
||||
// Global prefix
|
||||
app.setGlobalPrefix('v1', {
|
||||
exclude: ['health', 'health/ready', 'health/live'],
|
||||
});
|
||||
|
||||
const port = configService.get<number>('PORT') || 8080;
|
||||
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(`Environment: ${configService.get('NODE_ENV')}`);
|
||||
logger.log(`Mana Service URL: ${configService.get('MANA_SERVICE_URL')}`);
|
||||
logger.log(`App ID: ${configService.get('APP_ID')}`);
|
||||
logger.log(`Application running on port ${port}`);
|
||||
logger.log(`Health check available at http://localhost:${port}/health`);
|
||||
}
|
||||
bootstrap();
|
||||
283
manadeck/backend/src/services/supabase.service.ts
Normal file
283
manadeck/backend/src/services/supabase.service.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
@Injectable()
|
||||
export class SupabaseService {
|
||||
private readonly logger = new Logger(SupabaseService.name);
|
||||
private supabase: SupabaseClient;
|
||||
private supabaseServiceRole: SupabaseClient;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
const supabaseAnonKey = this.configService.get<string>('SUPABASE_ANON_KEY')!;
|
||||
const supabaseServiceKey = this.configService.get<string>('SUPABASE_SERVICE_KEY');
|
||||
|
||||
// Client for public operations
|
||||
this.supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Client for service-level operations (optional, only if service key is provided)
|
||||
if (supabaseServiceKey) {
|
||||
this.supabaseServiceRole = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('Supabase service initialized');
|
||||
}
|
||||
|
||||
// Get client with user's token for RLS
|
||||
getClientWithUserToken(token: string): SupabaseClient {
|
||||
const supabaseUrl = this.configService.get<string>('SUPABASE_URL')!;
|
||||
const supabaseAnonKey = this.configService.get<string>('SUPABASE_ANON_KEY')!;
|
||||
|
||||
return createClient(supabaseUrl, supabaseAnonKey, {
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Example methods for deck operations
|
||||
async getUserDecks(userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching user decks:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createDeck(userId: string, deckData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.insert({
|
||||
...deckData,
|
||||
user_id: userId,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error creating deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateDeck(deckId: string, userId: string, deckData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('decks')
|
||||
.update({
|
||||
...deckData,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.eq('id', deckId)
|
||||
.eq('user_id', userId) // Ensure user owns the deck
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error updating deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async deleteDeck(deckId: string, userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { error } = await client
|
||||
.from('decks')
|
||||
.delete()
|
||||
.eq('id', deckId)
|
||||
.eq('user_id', userId); // Ensure user owns the deck
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error deleting deck:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Example methods for card operations
|
||||
async getUserCards(userId: string, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('cards')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching user cards:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async createCard(userId: string, cardData: any, token?: string) {
|
||||
const client = token ? this.getClientWithUserToken(token) : this.supabase;
|
||||
|
||||
const { data, error } = await client
|
||||
.from('cards')
|
||||
.insert({
|
||||
...cardData,
|
||||
user_id: userId,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error creating card:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Public methods (no auth required)
|
||||
async getFeaturedDecks(limit = 10) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('decks')
|
||||
.select('*')
|
||||
.eq('is_featured', true)
|
||||
.eq('is_public', true)
|
||||
.limit(limit)
|
||||
.order('featured_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching featured decks:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getLeaderboard(limit = 10) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('user_stats')
|
||||
.select('*')
|
||||
.order('total_wins', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching leaderboard:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getDeckTemplates(category?: string) {
|
||||
let query = this.supabase
|
||||
.from('deck_templates')
|
||||
.select('*')
|
||||
.eq('is_active', true);
|
||||
|
||||
if (category) {
|
||||
query = query.eq('category', category);
|
||||
}
|
||||
|
||||
const { data, error } = await query.order('popularity', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching deck templates:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Service-level operations (using service role key)
|
||||
async adminGetAllUsers() {
|
||||
if (!this.supabaseServiceRole) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
|
||||
const { data, error } = await this.supabaseServiceRole.auth.admin.listUsers();
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error fetching all users:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Edge Function invocations
|
||||
async generateDeckWithAI(
|
||||
userId: string,
|
||||
requestData: {
|
||||
prompt: string;
|
||||
deckTitle: string;
|
||||
deckDescription?: string;
|
||||
cardCount?: number;
|
||||
cardTypes?: string[];
|
||||
difficulty?: string;
|
||||
tags?: string[];
|
||||
},
|
||||
manaToken: string,
|
||||
) {
|
||||
if (!this.supabaseServiceRole) {
|
||||
throw new Error('Service role key not configured');
|
||||
}
|
||||
|
||||
this.logger.log(`Invoking generate-deck edge function for user ${userId}`);
|
||||
|
||||
const { data, error } = await this.supabaseServiceRole.functions.invoke(
|
||||
'generate-deck',
|
||||
{
|
||||
body: requestData,
|
||||
headers: {
|
||||
Authorization: `Bearer ${manaToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
this.logger.error('Error invoking generate-deck edge function:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
25
manadeck/backend/test/app.e2e-spec.ts
Normal file
25
manadeck/backend/test/app.e2e-spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
9
manadeck/backend/test/jest-e2e.json
Normal file
9
manadeck/backend/test/jest-e2e.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
manadeck/backend/tsconfig.build.json
Normal file
4
manadeck/backend/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
25
manadeck/backend/tsconfig.json
Normal file
25
manadeck/backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
220
manadeck/backend/verify-build.sh
Executable file
220
manadeck/backend/verify-build.sh
Executable file
|
|
@ -0,0 +1,220 @@
|
|||
#!/bin/bash
|
||||
# Script to verify the build and local Docker testing for Manadeck Backend
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "=========================================="
|
||||
echo "Manadeck Backend Build Verification"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print success
|
||||
success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
# Function to print error
|
||||
error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
# Function to print warning
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
# Check current directory
|
||||
echo "1. Checking directory..."
|
||||
if [ ! -f "package.json" ]; then
|
||||
error "package.json not found. Please run this script from manadeck/backend directory"
|
||||
exit 1
|
||||
fi
|
||||
success "Running from correct directory: $(pwd)"
|
||||
echo ""
|
||||
|
||||
# Check for required files
|
||||
echo "2. Checking required files..."
|
||||
REQUIRED_FILES=("Dockerfile" "cloudbuild.yaml" "tsconfig.json" "nest-cli.json" "src/main.ts")
|
||||
for file in "${REQUIRED_FILES[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
success "$file exists"
|
||||
else
|
||||
error "$file not found"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Check Node.js version
|
||||
echo "3. Checking Node.js version..."
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION=$(node -v)
|
||||
success "Node.js version: $NODE_VERSION"
|
||||
else
|
||||
error "Node.js not installed"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Install dependencies
|
||||
echo "4. Installing dependencies..."
|
||||
if [ ! -d "node_modules" ]; then
|
||||
warning "node_modules not found. Running npm ci..."
|
||||
npm ci
|
||||
else
|
||||
success "node_modules exists"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run linter
|
||||
echo "5. Running linter..."
|
||||
if npm run lint; then
|
||||
success "Linting passed"
|
||||
else
|
||||
warning "Linting failed (non-blocking)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Build the project
|
||||
echo "6. Building the project..."
|
||||
if npm run build; then
|
||||
success "Build successful"
|
||||
else
|
||||
error "Build failed"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check dist directory
|
||||
echo "7. Checking build output..."
|
||||
if [ -d "dist" ]; then
|
||||
success "dist directory exists"
|
||||
|
||||
if [ -f "dist/main.js" ]; then
|
||||
success "dist/main.js exists"
|
||||
FILE_SIZE=$(du -h dist/main.js | cut -f1)
|
||||
echo " File size: $FILE_SIZE"
|
||||
else
|
||||
error "dist/main.js not found"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
error "dist directory not found"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check for .env file
|
||||
echo "8. Checking environment configuration..."
|
||||
if [ -f ".env" ]; then
|
||||
success ".env file exists"
|
||||
else
|
||||
warning ".env file not found (required for local testing)"
|
||||
echo " Create .env from .env.example for local development"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Docker build test (optional)
|
||||
echo "9. Docker build test..."
|
||||
read -p "Do you want to test Docker build? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Building Docker image..."
|
||||
|
||||
if command -v docker &> /dev/null; then
|
||||
if docker build -t manadeck-backend:test .; then
|
||||
success "Docker build successful"
|
||||
|
||||
# Optional: Run container for health check
|
||||
read -p "Do you want to test the Docker container? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Starting Docker container..."
|
||||
|
||||
# Check if .env exists for env vars
|
||||
if [ -f ".env" ]; then
|
||||
docker run -d --name manadeck-test -p 8080:8080 --env-file .env manadeck-backend:test
|
||||
else
|
||||
warning "No .env file found. Starting container without environment variables..."
|
||||
docker run -d --name manadeck-test -p 8080:8080 manadeck-backend:test
|
||||
fi
|
||||
|
||||
echo "Waiting for service to start..."
|
||||
sleep 5
|
||||
|
||||
# Test health endpoint
|
||||
echo "Testing health endpoint..."
|
||||
if curl -s http://localhost:8080/health > /dev/null; then
|
||||
success "Health check passed"
|
||||
curl http://localhost:8080/health | jq . || cat
|
||||
else
|
||||
warning "Health check failed (may need environment variables)"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
echo ""
|
||||
echo "Stopping and removing test container..."
|
||||
docker stop manadeck-test > /dev/null 2>&1
|
||||
docker rm manadeck-test > /dev/null 2>&1
|
||||
success "Cleanup complete"
|
||||
fi
|
||||
|
||||
# Optional: Remove test image
|
||||
read -p "Do you want to remove the test Docker image? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
docker rmi manadeck-backend:test > /dev/null 2>&1
|
||||
success "Test image removed"
|
||||
fi
|
||||
else
|
||||
error "Docker build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
warning "Docker not installed. Skipping Docker build test."
|
||||
fi
|
||||
else
|
||||
echo "Skipping Docker build test"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check cloudbuild.yaml version
|
||||
echo "10. Checking cloudbuild.yaml version..."
|
||||
if [ -f "cloudbuild.yaml" ]; then
|
||||
CURRENT_VERSION=$(grep -o "manadeck-backend:v[0-9.]*" cloudbuild.yaml | head -1 | sed 's/manadeck-backend://')
|
||||
success "Current version in cloudbuild.yaml: $CURRENT_VERSION"
|
||||
|
||||
warning "Before deploying, consider incrementing the version in cloudbuild.yaml"
|
||||
else
|
||||
error "cloudbuild.yaml not found"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "=========================================="
|
||||
echo "Build Verification Summary"
|
||||
echo "=========================================="
|
||||
success "All required files present"
|
||||
success "Dependencies installed"
|
||||
success "Build completed successfully"
|
||||
success "Build artifacts verified"
|
||||
echo ""
|
||||
|
||||
echo "Next steps:"
|
||||
echo "1. Review and update version in cloudbuild.yaml if needed"
|
||||
echo "2. Commit your changes to git"
|
||||
echo "3. Push to main branch to trigger automatic deployment"
|
||||
echo " OR"
|
||||
echo " Deploy manually:"
|
||||
echo " cd $(pwd)"
|
||||
echo " gcloud builds submit --project=memo-2c4c4 --config=cloudbuild.yaml ."
|
||||
echo ""
|
||||
|
||||
echo "For deployment help, see DEPLOY_MANUAL.md"
|
||||
echo ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue