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:
Till-JS 2025-11-22 23:38:24 +01:00
commit e7f5f942f3
2450 changed files with 487047 additions and 0 deletions

View 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

View 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

View 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
View file

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View 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
View 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"]

View 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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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).

View 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

View 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. 🎯

View 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'

View 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 ""

View 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'
},
},
);

View 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"
}

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View 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"
}
}

View 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 ""

View 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!');
});
});
});

View 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();
}
}

View 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
}
}

View file

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View 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;
}

View 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(),
});

View 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',
};
}
}

View 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'),
};
}
}

View 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(),
},
],
};
}
}

View 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();

View 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;
}
}

View 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!');
});
});

View file

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View 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
View 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 ""