Update cors origin

This commit is contained in:
dvirlabs 2026-01-26 04:06:16 +02:00
commit abc9e94104
29 changed files with 1672 additions and 19 deletions

View File

@ -2,7 +2,7 @@
DB_USER=recipes_user DB_USER=recipes_user
DB_PASSWORD=recipes_password DB_PASSWORD=recipes_password
DB_NAME=recipes_db DB_NAME=recipes_db
DB_HOST=my-recipes-rds.chw4omcqsuqv7.eu-central-1.rds.amazonaws.com DB_HOST=my-recipes-rds.chw4omcguqv7.eu-central-1.rds.amazonaws.com
DB_PORT=5432 DB_PORT=5432
# Email Configuration # Email Configuration
@ -24,11 +24,12 @@ AZURE_CLIENT_SECRET=Zad8Q~qRBxaQq8up0lLXAq4pHzrVM2JFGFJhHaDp
AZURE_TENANT_ID=consumers AZURE_TENANT_ID=consumers
AZURE_REDIRECT_URI=http://localhost:8000/auth/azure/callback AZURE_REDIRECT_URI=http://localhost:8000/auth/azure/callback
# Cloudflare R2 Backup Configuration # AWS S3 Backup Configuration
R2_ENDPOINT=https://d4704b8c40b2f95b2c7bf7ee4ecc52f8.r2.cloudflarestorage.com S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
R2_ACCESS_KEY=1997b1e48a337c0dbe1f7552a08631b5 S3_ACCESS_KEY=1997b1e48a337c0dbe1f7552a08631b5
R2_SECRET_KEY=369694e39fedfedb254158c147171f5760de84fa2346d5d5d5a961f1f517dbc6 S3_SECRET_KEY=369694e39fedfedb254158c147171f5760de84fa2346d5d5d5a961f1f517dbc6
R2_BUCKET_NAME=recipes-backups S3_BUCKET_NAME=recipes-backups
S3_REGION=eu-central-1
# Automatic Backup Schedule # Automatic Backup Schedule
# Options: test (every 1 minute), daily, weekly, disabled # Options: test (every 1 minute), daily, weekly, disabled

225
aws/EKS_DEPLOYMENT.md Normal file
View File

@ -0,0 +1,225 @@
# AWS EKS Deployment Guide
This directory contains the Helm chart and configuration for deploying My Recipes application to Amazon EKS (Elastic Kubernetes Service).
## Structure
```
aws/
├── my-recipes-chart/ # Base Helm chart with default values
│ ├── Chart.yaml
│ ├── values.yaml # Base configuration (don't modify directly)
│ └── templates/ # Kubernetes resource templates
└── values.yaml # Project-specific values (override base values)
```
## Prerequisites
1. **AWS CLI** - Configured with appropriate credentials
2. **kubectl** - Kubernetes command-line tool
3. **Helm 3** - Package manager for Kubernetes
4. **eksctl** (optional) - For creating EKS clusters
## Setup Steps
### 1. Create EKS Cluster (if not already exists)
```bash
eksctl create cluster \
--name my-recipes-cluster \
--region eu-central-1 \
--nodegroup-name standard-workers \
--node-type t3.medium \
--nodes 2 \
--nodes-min 1 \
--nodes-max 3
```
### 2. Configure kubectl
```bash
aws eks update-kubeconfig --region eu-central-1 --name my-recipes-cluster
```
### 3. Create Namespace
```bash
kubectl create namespace my-apps
```
### 4. Install Ingress Controller (if not already installed)
For AWS ALB Ingress Controller:
```bash
# Install AWS Load Balancer Controller
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=my-recipes-cluster
```
Or for NGINX Ingress Controller:
```bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install nginx-ingress ingress-nginx/ingress-nginx \
-n ingress-nginx --create-namespace
```
### 5. Install cert-manager (for SSL certificates)
```bash
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set installCRDs=true
```
### 6. Configure values.yaml
Edit `values.yaml` in this directory and update:
- **Container images**: Update ECR repository URLs
- **Domain names**: Replace `<YOUR_DOMAIN>` with your actual domain
- **S3 credentials**: Add your AWS access key and secret key
- **Database**: Configure RDS connection details
- **OAuth**: Update redirect URIs with your domain
### 7. Create S3 Bucket for Backups
```bash
aws s3 mb s3://my-recipes-backups --region eu-central-1
```
### 8. Push Docker Images to ECR
```bash
# Create ECR repositories
aws ecr create-repository --repository-name my-recipes-backend --region eu-central-1
aws ecr create-repository --repository-name my-recipes-frontend --region eu-central-1
# Login to ECR
aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin <AWS_ACCOUNT_ID>.dkr.ecr.eu-central-1.amazonaws.com
# Build and push backend
cd backend
docker build -t my-recipes-backend .
docker tag my-recipes-backend:latest <AWS_ACCOUNT_ID>.dkr.ecr.eu-central-1.amazonaws.com/my-recipes-backend:latest
docker push <AWS_ACCOUNT_ID>.dkr.ecr.eu-central-1.amazonaws.com/my-recipes-backend:latest
# Build and push frontend
cd ../frontend
docker build -t my-recipes-frontend .
docker tag my-recipes-frontend:latest <AWS_ACCOUNT_ID>.dkr.ecr.eu-central-1.amazonaws.com/my-recipes-frontend:latest
docker push <AWS_ACCOUNT_ID>.dkr.ecr.eu-central-1.amazonaws.com/my-recipes-frontend:latest
```
### 9. Deploy with Helm
```bash
# From the aws directory
helm install my-recipes ./my-recipes-chart \
-f values.yaml \
-n my-apps
```
### 10. Verify Deployment
```bash
# Check pods
kubectl get pods -n my-apps
# Check services
kubectl get svc -n my-apps
# Check ingress
kubectl get ingress -n my-apps
# View logs
kubectl logs -f deployment/my-recipes-backend -n my-apps
```
## Upgrading
To update the deployment:
```bash
# Update values.yaml with new configuration
helm upgrade my-recipes ./my-recipes-chart \
-f values.yaml \
-n my-apps
```
## Using AWS RDS (Recommended for Production)
1. Create RDS PostgreSQL instance
2. Configure security groups to allow EKS node group access
3. Update `database` section in `values.yaml` with RDS connection details
4. The chart will automatically use external database instead of in-cluster PostgreSQL
## Using S3 for Backups
The application is configured to use AWS S3 for database backups instead of Cloudflare R2. Ensure:
1. S3 bucket exists and is accessible
2. AWS credentials have appropriate permissions:
- `s3:PutObject`
- `s3:GetObject`
- `s3:ListBucket`
- `s3:DeleteObject`
## Environment Variables
The chart automatically creates secrets from `values.yaml`:
- Database credentials
- OAuth client secrets
- Email SMTP credentials
- S3 access keys
All sensitive data should be stored in AWS Secrets Manager in production and referenced via External Secrets Operator.
## Monitoring
To view application logs:
```bash
# Backend logs
kubectl logs -f deployment/my-recipes-backend -n my-apps
# Frontend logs
kubectl logs -f deployment/my-recipes-frontend -n my-apps
# Database logs (if using in-cluster DB)
kubectl logs -f statefulset/my-recipes-db -n my-apps
```
## Troubleshooting
### Pods not starting
```bash
kubectl describe pod <pod-name> -n my-apps
```
### Database connection issues
```bash
kubectl exec -it deployment/my-recipes-backend -n my-apps -- env | grep DB_
```
### Ingress not working
```bash
kubectl describe ingress -n my-apps
```
## Uninstall
```bash
helm uninstall my-recipes -n my-apps
```
## Cost Optimization
For non-production environments:
- Reduce replica counts to 1
- Use smaller instance types (t3.small)
- Use in-cluster PostgreSQL instead of RDS
- Configure cluster autoscaling

View File

@ -0,0 +1,14 @@
apiVersion: v2
name: my-recipes
description: Complete recipe management application with PostgreSQL, FastAPI backend, and React frontend
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- recipes
- fastapi
- react
- postgresql
maintainers:
- name: Development Team

View File

@ -0,0 +1,45 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-add-missing-tables
namespace: {{ .Values.global.namespace }}
data:
add-tables.sql: |
-- Create grocery lists table
CREATE TABLE IF NOT EXISTS grocery_lists (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
items TEXT[] NOT NULL DEFAULT '{}',
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
is_pinned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create grocery list shares table
CREATE TABLE IF NOT EXISTS grocery_list_shares (
id SERIAL PRIMARY KEY,
list_id INTEGER NOT NULL REFERENCES grocery_lists(id) ON DELETE CASCADE,
shared_with_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
can_edit BOOLEAN DEFAULT FALSE,
shared_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(list_id, shared_with_user_id)
);
CREATE INDEX IF NOT EXISTS idx_grocery_lists_owner_id ON grocery_lists (owner_id);
CREATE INDEX IF NOT EXISTS idx_grocery_list_shares_list_id ON grocery_list_shares (list_id);
CREATE INDEX IF NOT EXISTS idx_grocery_list_shares_user_id ON grocery_list_shares (shared_with_user_id);
-- Create notifications table
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type TEXT NOT NULL,
message TEXT NOT NULL,
related_id INTEGER,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications (user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications (is_read);

View File

@ -0,0 +1,49 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-add-missing-tables
namespace: {{ .Values.global.namespace }}
annotations:
"helm.sh/hook": post-upgrade
"helm.sh/hook-weight": "6"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
restartPolicy: Never
containers:
- name: add-tables
image: postgres:16-alpine
env:
- name: PGHOST
value: {{ .Release.Name }}-db
- name: PGPORT
value: "{{ .Values.postgres.port }}"
- name: PGDATABASE
value: {{ .Values.postgres.database }}
- name: PGUSER
value: {{ .Values.postgres.user }}
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PASSWORD
command:
- sh
- -c
- |
echo "Waiting for database to be ready..."
until pg_isready -h $PGHOST -p $PGPORT -U $PGUSER; do
echo "Database not ready, waiting..."
sleep 2
done
echo "Database ready, adding missing tables..."
psql -v ON_ERROR_STOP=1 -f /sql/add-tables.sql
echo "Tables added successfully!"
volumeMounts:
- name: sql
mountPath: /sql
volumes:
- name: sql
configMap:
name: {{ .Release.Name }}-add-missing-tables

View File

@ -0,0 +1,99 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-admin-init
namespace: {{ .Values.global.namespace }}
data:
create-admin.py: |
#!/usr/bin/env python3
import os
import sys
import psycopg2
import bcrypt
from time import sleep
def wait_for_db():
"""Wait for database to be ready"""
max_retries = 30
retry_count = 0
while retry_count < max_retries:
try:
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ['DB_PORT'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD']
)
conn.close()
print("✓ Database is ready")
return True
except Exception as e:
retry_count += 1
print(f"Waiting for database... ({retry_count}/{max_retries})")
sleep(2)
print("✗ Database connection timeout")
return False
def create_admin_user():
"""Create admin user if not exists"""
try:
# Hash the password
password = os.environ.get('ADMIN_PASSWORD', 'admin123')
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
# Connect to database
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ['DB_PORT'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD']
)
cur = conn.cursor()
# Insert admin user
cur.execute("""
INSERT INTO users (username, email, password_hash, first_name, last_name, display_name, is_admin)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
password_hash = EXCLUDED.password_hash,
first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
display_name = EXCLUDED.display_name,
is_admin = EXCLUDED.is_admin
""", (
os.environ.get('ADMIN_USERNAME', 'admin'),
os.environ.get('ADMIN_EMAIL', 'admin@myrecipes.local'),
password_hash,
os.environ.get('ADMIN_FIRST_NAME', 'Admin'),
os.environ.get('ADMIN_LAST_NAME', 'User'),
os.environ.get('ADMIN_DISPLAY_NAME', 'מנהל'),
True
))
conn.commit()
cur.close()
conn.close()
print(f"✓ Admin user '{os.environ.get('ADMIN_USERNAME', 'admin')}' created/updated successfully")
return True
except Exception as e:
print(f"✗ Error creating admin user: {e}")
return False
if __name__ == "__main__":
print("Starting admin user initialization...")
if not wait_for_db():
sys.exit(1)
if not create_admin_user():
sys.exit(1)
print("✓ Admin user initialization completed")
sys.exit(0)

View File

@ -0,0 +1,75 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-admin-init-{{ .Release.Revision }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-admin-init
component: init
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "10"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: {{ .Release.Name }}-admin-init
spec:
restartPolicy: Never
containers:
- name: admin-init
image: python:3.12-slim
command:
- /bin/sh
- -c
- |
pip install --no-cache-dir psycopg2-binary bcrypt > /dev/null 2>&1
python3 /scripts/create-admin.py
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_HOST
- name: DB_PORT
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PORT
- name: DB_NAME
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_NAME
- name: DB_USER
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PASSWORD
- name: ADMIN_USERNAME
value: {{ .Values.admin.username | quote }}
- name: ADMIN_EMAIL
value: {{ .Values.admin.email | quote }}
- name: ADMIN_PASSWORD
value: {{ .Values.admin.password | quote }}
- name: ADMIN_FIRST_NAME
value: {{ .Values.admin.firstName | quote }}
- name: ADMIN_LAST_NAME
value: {{ .Values.admin.lastName | quote }}
- name: ADMIN_DISPLAY_NAME
value: {{ .Values.admin.displayName | quote }}
volumeMounts:
- name: init-script
mountPath: /scripts
volumes:
- name: init-script
configMap:
name: {{ .Release.Name }}-admin-init
defaultMode: 0755

View File

@ -0,0 +1,35 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-app-secrets
namespace: {{ .Values.global.namespace }}
type: Opaque
stringData:
# Google OAuth
GOOGLE_CLIENT_ID: {{ .Values.oauth.google.clientId | quote }}
GOOGLE_CLIENT_SECRET: {{ .Values.oauth.google.clientSecret | quote }}
GOOGLE_REDIRECT_URI: {{ .Values.oauth.google.redirectUri | quote }}
# Microsoft Entra ID (Azure AD) OAuth
AZURE_CLIENT_ID: {{ .Values.oauth.azure.clientId | quote }}
AZURE_CLIENT_SECRET: {{ .Values.oauth.azure.clientSecret | quote }}
AZURE_TENANT_ID: {{ .Values.oauth.azure.tenantId | quote }}
AZURE_REDIRECT_URI: {{ .Values.oauth.azure.redirectUri | quote }}
# Email Configuration
SMTP_HOST: {{ .Values.email.smtpHost | quote }}
SMTP_PORT: {{ .Values.email.smtpPort | quote }}
SMTP_USER: {{ .Values.email.smtpUser | quote }}
SMTP_PASSWORD: {{ .Values.email.smtpPassword | quote }}
SMTP_FROM: {{ .Values.email.smtpFrom | quote }}
# Frontend URL for redirects
FRONTEND_URL: {{ .Values.frontend.externalUrl | quote }}
# S3 Backup Configuration
S3_ENDPOINT: {{ .Values.s3.endpoint | quote }}
S3_ACCESS_KEY: {{ .Values.s3.accessKey | quote }}
S3_SECRET_KEY: {{ .Values.s3.secretKey | quote }}
S3_BUCKET_NAME: {{ .Values.s3.bucketName | quote }}
S3_REGION: {{ .Values.s3.region | quote }}
BACKUP_INTERVAL: {{ .Values.s3.backupInterval | quote }}

View File

@ -0,0 +1,119 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Values.backend.name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.backend.name }}
component: backend
spec:
replicas: {{ .Values.backend.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Values.backend.name }}
template:
metadata:
labels:
app: {{ .Release.Name }}-{{ .Values.backend.name }}
component: backend
spec:
initContainers:
- name: db-migration
image: postgres:16-alpine
command:
- /bin/sh
- -c
- |
echo "Waiting for database to be ready..."
until pg_isready -h $DB_HOST -U $DB_USER; do
echo "Database not ready, waiting..."
sleep 2
done
echo "Database is ready, running migration..."
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f /migration/migrate.sql
echo "Migration completed successfully"
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_HOST
- name: DB_PORT
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PORT
- name: DB_NAME
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_NAME
- name: DB_USER
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PASSWORD
volumeMounts:
- name: migration-script
mountPath: /migration
containers:
- name: {{ .Values.backend.name }}
image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
ports:
- containerPort: {{ .Values.backend.service.targetPort }}
name: http
protocol: TCP
env:
{{- if .Values.backend.env }}
{{- range $key, $value := .Values.backend.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
envFrom:
- secretRef:
name: {{ .Release.Name }}-db-credentials
- secretRef:
name: {{ .Release.Name }}-app-secrets
startupProbe:
httpGet:
path: /docs
port: http
initialDelaySeconds: 15
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
livenessProbe:
httpGet:
path: /docs
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /docs
port: http
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
resources:
requests:
cpu: {{ .Values.backend.resources.requests.cpu }}
memory: {{ .Values.backend.resources.requests.memory }}
limits:
cpu: {{ .Values.backend.resources.limits.cpu }}
memory: {{ .Values.backend.resources.limits.memory }}
volumes:
- name: migration-script
configMap:
name: {{ .Release.Name }}-db-migration

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Values.backend.name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.backend.name }}
component: backend
spec:
type: {{ .Values.backend.service.type }}
ports:
- port: {{ .Values.backend.service.port }}
targetPort: {{ .Values.backend.service.targetPort }}
protocol: TCP
name: http
selector:
app: {{ .Release.Name }}-{{ .Values.backend.name }}

View File

@ -0,0 +1,54 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-db-migration
namespace: {{ .Values.global.namespace }}
data:
migrate.sql: |
-- Add made_by column to recipes if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'recipes' AND column_name = 'made_by'
) THEN
ALTER TABLE recipes ADD COLUMN made_by TEXT;
END IF;
END $$;
-- Create index if it doesn't exist
CREATE INDEX IF NOT EXISTS idx_recipes_made_by ON recipes (made_by);
-- Add is_admin column to users if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'is_admin'
) THEN
ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE;
END IF;
END $$;
-- Add auth_provider column to users if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'auth_provider'
) THEN
ALTER TABLE users ADD COLUMN auth_provider TEXT DEFAULT 'local';
END IF;
END $$;
-- Verify recipes schema
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'recipes'
ORDER BY ordinal_position;
-- Verify users schema
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position;

View File

@ -0,0 +1,75 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migration-{{ .Release.Revision }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-db-migration
component: migration
annotations:
"helm.sh/hook": post-upgrade,post-install
"helm.sh/hook-weight": "5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: {{ .Release.Name }}-db-migration
spec:
restartPolicy: Never
containers:
- name: migrate
image: postgres:16-alpine
command:
- /bin/sh
- -c
- |
echo "Waiting for database to be ready..."
until pg_isready -h $DB_HOST -U $DB_USER; do
echo "Database not ready, waiting..."
sleep 2
done
echo "Database is ready, applying schema..."
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f /schema/schema.sql
echo "Schema applied, running migrations..."
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_NAME -f /migration/migrate.sql
echo "Migration completed successfully"
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_HOST
- name: DB_PORT
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PORT
- name: DB_NAME
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_NAME
- name: DB_USER
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-db-credentials
key: DB_PASSWORD
volumeMounts:
- name: migration-script
mountPath: /migration
- name: schema-script
mountPath: /schema
volumes:
- name: migration-script
configMap:
name: {{ .Release.Name }}-db-migration
- name: schema-script
configMap:
name: {{ .Release.Name }}-db-schema

View File

@ -0,0 +1,134 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-db-schema
namespace: {{ .Values.global.namespace }}
data:
schema.sql: |
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
display_name TEXT NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
auth_provider TEXT DEFAULT 'local',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users (username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
-- Create recipes table (matching backend schema with TEXT[] arrays)
CREATE TABLE IF NOT EXISTS recipes (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
meal_type TEXT NOT NULL, -- breakfast / lunch / dinner / snack
time_minutes INTEGER NOT NULL,
tags TEXT[] NOT NULL DEFAULT '{}', -- {"מהיר", "בריא"}
ingredients TEXT[] NOT NULL DEFAULT '{}', -- {"ביצה", "עגבניה", "מלח"}
steps TEXT[] NOT NULL DEFAULT '{}', -- {"לחתוך", "לבשל", ...}
image TEXT, -- Base64-encoded image or image URL
made_by TEXT, -- Person who created this recipe version
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, -- Recipe owner
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for filters
CREATE INDEX IF NOT EXISTS idx_recipes_meal_type
ON recipes (meal_type);
CREATE INDEX IF NOT EXISTS idx_recipes_time_minutes
ON recipes (time_minutes);
CREATE INDEX IF NOT EXISTS idx_recipes_made_by
ON recipes (made_by);
CREATE INDEX IF NOT EXISTS idx_recipes_user_id
ON recipes (user_id);
-- Add new columns to existing users table
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'first_name'
) THEN
ALTER TABLE users ADD COLUMN first_name TEXT;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'last_name'
) THEN
ALTER TABLE users ADD COLUMN last_name TEXT;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'display_name'
) THEN
ALTER TABLE users ADD COLUMN display_name TEXT;
-- Set display_name to username for existing users
UPDATE users SET display_name = username WHERE display_name IS NULL;
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'is_admin'
) THEN
ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE;
END IF;
END $$;
-- Create grocery lists table
CREATE TABLE IF NOT EXISTS grocery_lists (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
items TEXT[] NOT NULL DEFAULT '{}',
owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
is_pinned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create grocery list shares table
CREATE TABLE IF NOT EXISTS grocery_list_shares (
id SERIAL PRIMARY KEY,
list_id INTEGER NOT NULL REFERENCES grocery_lists(id) ON DELETE CASCADE,
shared_with_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
can_edit BOOLEAN DEFAULT FALSE,
shared_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(list_id, shared_with_user_id)
);
CREATE INDEX IF NOT EXISTS idx_grocery_lists_owner_id ON grocery_lists (owner_id);
CREATE INDEX IF NOT EXISTS idx_grocery_list_shares_list_id ON grocery_list_shares (list_id);
CREATE INDEX IF NOT EXISTS idx_grocery_list_shares_user_id ON grocery_list_shares (shared_with_user_id);
-- Create notifications table
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type TEXT NOT NULL,
message TEXT NOT NULL,
related_id INTEGER,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications (user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications (is_read);

View File

@ -0,0 +1,23 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-db-credentials
namespace: {{ .Values.global.namespace }}
type: Opaque
stringData:
{{- if .Values.database }}
# External database (e.g., AWS RDS)
DB_HOST: {{ .Values.database.host | quote }}
DB_PORT: {{ .Values.database.port | quote }}
DB_NAME: {{ .Values.database.name | quote }}
DB_USER: {{ .Values.database.user | quote }}
DB_PASSWORD: {{ .Values.database.password | quote }}
{{- else }}
# In-cluster PostgreSQL
DB_HOST: {{ printf "%s-%s-headless.%s.svc.cluster.local" .Release.Name .Values.postgres.name .Values.global.namespace }}
DB_PORT: "{{ .Values.postgres.port }}"
DB_NAME: {{ .Values.postgres.database | quote }}
DB_USER: {{ .Values.postgres.user | quote }}
DB_PASSWORD: {{ .Values.postgres.password | quote }}
{{- end }}

View File

@ -0,0 +1,39 @@
{{- if not .Values.database }}
{{- /* Only deploy in-cluster PostgreSQL services if external database is not configured */ -}}
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Values.postgres.name }}-headless
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
component: database
spec:
clusterIP: None
selector:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
ports:
- name: postgres
port: {{ .Values.postgres.port }}
targetPort: {{ .Values.postgres.port }}
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Values.postgres.name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
component: database
spec:
type: {{ .Values.postgres.service.type }}
selector:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
ports:
- name: postgres
port: {{ .Values.postgres.service.port }}
targetPort: {{ .Values.postgres.port }}
protocol: TCP
{{- end }}

View File

@ -0,0 +1,89 @@
{{- if not .Values.database }}
{{- /* Only deploy in-cluster PostgreSQL if external database is not configured */ -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-{{ .Values.postgres.name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
component: database
spec:
serviceName: {{ .Release.Name }}-{{ .Values.postgres.name }}-headless
replicas: 1
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
template:
metadata:
labels:
app: {{ .Release.Name }}-{{ .Values.postgres.name }}
component: database
spec:
containers:
- name: postgres
image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}"
imagePullPolicy: {{ .Values.postgres.image.pullPolicy }}
ports:
- containerPort: {{ .Values.postgres.port }}
name: postgres
protocol: TCP
env:
- name: POSTGRES_USER
value: {{ .Values.postgres.user | quote }}
- name: POSTGRES_PASSWORD
value: {{ .Values.postgres.password | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgres.database | quote }}
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: init-sql
mountPath: /docker-entrypoint-initdb.d
livenessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U {{ .Values.postgres.user }}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U {{ .Values.postgres.user }}
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
resources:
requests:
cpu: {{ .Values.postgres.resources.requests.cpu }}
memory: {{ .Values.postgres.resources.requests.memory }}
limits:
cpu: {{ .Values.postgres.resources.limits.cpu }}
memory: {{ .Values.postgres.resources.limits.memory }}
volumes:
- name: init-sql
configMap:
name: {{ .Release.Name }}-db-schema
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- {{ .Values.postgres.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.postgres.persistence.size }}
{{- if .Values.postgres.persistence.storageClass }}
storageClassName: {{ .Values.postgres.persistence.storageClass | quote }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Values.frontend.name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.frontend.name }}
component: frontend
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Values.frontend.name }}
template:
metadata:
labels:
app: {{ .Release.Name }}-{{ .Values.frontend.name }}
component: frontend
spec:
containers:
- name: {{ .Values.frontend.name }}
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- containerPort: {{ .Values.frontend.service.targetPort }}
name: http
protocol: TCP
{{- with .Values.frontend.env }}
env:
{{- range $key, $value := . }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
resources:
requests:
cpu: {{ .Values.frontend.resources.requests.cpu }}
memory: {{ .Values.frontend.resources.requests.memory }}
limits:
cpu: {{ .Values.frontend.resources.limits.cpu }}
memory: {{ .Values.frontend.resources.limits.memory }}

View File

@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-{{ .Values.frontend.name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}-{{ .Values.frontend.name }}
component: frontend
spec:
type: {{ .Values.frontend.service.type }}
ports:
- port: {{ .Values.frontend.service.port }}
targetPort: {{ .Values.frontend.service.targetPort }}
protocol: TCP
name: http
selector:
app: {{ .Release.Name }}-{{ .Values.frontend.name }}

View File

@ -0,0 +1,48 @@
{{- if .Values.frontend.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}
namespace: {{ .Values.global.namespace }}
labels:
app: {{ .Release.Name }}
{{- with .Values.frontend.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.frontend.ingress.className }}
ingressClassName: {{ .Values.frontend.ingress.className }}
{{- end }}
rules:
# Frontend rule
{{- range .Values.frontend.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ $.Release.Name }}-{{ $.Values.frontend.name }}
port:
number: {{ $.Values.frontend.service.port }}
{{- end }}
{{- end }}
# Backend API rule
{{- range .Values.backend.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ $.Release.Name }}-{{ $.Values.backend.name }}
port:
number: {{ $.Values.backend.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,182 @@
global:
namespace: my-apps
imagePullSecrets: []
# Backend configuration
backend:
name: backend
replicaCount: 2
image:
repository: harbor.dvirlabs.com/my-apps/my-recipes-backend
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 8000
targetPort: 8000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
env:
PYTHONUNBUFFERED: "1"
# Secrets are created in db-secret.yaml
# These are passed via envFrom secretRef
ingress:
enabled: true
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: "" # Set in project-specific values
hosts:
- host: api-my-recipes.dvirlabs.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-my-recipes-tls
hosts:
- api-my-recipes.dvirlabs.com
# Frontend configuration
frontend:
name: frontend
replicaCount: 2
image:
repository: harbor.dvirlabs.com/my-apps/my-recipes-frontend
pullPolicy: IfNotPresent
tag: "latest"
service:
type: ClusterIP
port: 80
targetPort: 80
env:
API_BASE: "https://api-my-recipes.dvirlabs.com"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
ingress:
enabled: true
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: "" # Set in project-specific values
hosts:
- host: my-recipes.dvirlabs.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: my-recipes-tls
hosts:
- my-recipes.dvirlabs.com
externalUrl: "https://my-recipes.dvirlabs.com"
# PostgreSQL configuration
postgres:
name: db
image:
repository: postgres
tag: "16-alpine"
pullPolicy: IfNotPresent
user: recipes_user
password: recipes_password
database: recipes_db
port: 5432
service:
type: ClusterIP
port: 5432
targetPort: 5432
persistence:
enabled: true
accessMode: ReadWriteOnce
storageClass: "nfs-client"
size: 10Gi
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
# OAuth Configuration
oauth:
google:
clientId: "143092846986-hsi59m0on2c9rb5qrdoejfceieao2ioc.apps.googleusercontent.com"
clientSecret: "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"
redirectUri: "https://api-my-recipes.dvirlabs.com/auth/google/callback"
azure:
clientId: "db244cf5-eb11-4738-a2ea-5b0716c9ec0a"
clientSecret: "Zad8Q~qRBxaQq8up0lLXAq4pHzrVM2JFGFJhHaDp"
tenantId: "consumers"
redirectUri: "https://api-my-recipes.dvirlabs.com/auth/azure/callback"
# Email Configuration
email:
smtpHost: "smtp.gmail.com"
smtpPort: "587"
smtpUser: "dvirlabs@gmail.com"
smtpPassword: "agaanrhbbazbdytv"
smtpFrom: "dvirlabs@gmail.com"
# S3 Backup Configuration
s3:
endpoint: "https://s3.amazonaws.com" # Can be overridden for specific regions
accessKey: "" # Set this in project-specific values.yaml
secretKey: "" # Set this in project-specific values.yaml
bucketName: "" # Set this in project-specific values.yaml
region: "us-east-1" # Set this in project-specific values.yaml
backupInterval: "weekly" # Options: test (1 min), daily, weekly
# Admin User Configuration
admin:
username: "admin"
email: "admin@example.com"
password: "admin123" # Change this in production!
firstName: "Admin"
lastName: "User"
displayName: "Admin User"
# Ingress configuration
ingress:
enabled: false # Individual frontend/backend ingress resources handle routing instead
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: my-recipes.dvirlabs.com
paths:
- path: /
pathType: Prefix
backend: frontend
tls:
- secretName: recipes-tls
hosts:
- my-recipes.dvirlabs.com

110
aws/final-app/values.yaml Normal file
View File

@ -0,0 +1,110 @@
# Project-specific values for AWS EKS deployment
# This file overrides the base values in my-recipes-chart/values.yaml
global:
namespace: my-apps
# Backend configuration
backend:
replicaCount: 2
image:
repository: 430842105273.dkr.ecr.eu-central-1.amazonaws.com/my-recipes-backend # Update with your ECR repository
tag: "latest"
ingress:
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
# Add your ACM certificate ARN below if you have one
# alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:..."
hosts:
- host: api-my-recipes.aws-dvirlabs.com
paths:
- path: /
pathType: Prefix
# Frontend configuration
frontend:
replicaCount: 2
image:
repository: 430842105273.dkr.ecr.eu-central-1.amazonaws.com/my-recipes-frontend # Update with your ECR repository
tag: "latest"
env:
API_BASE: "https://api-my-recipes.aws-dvirlabs.com"
ingress:
className: "alb"
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
# Add your ACM certificate ARN below if you have one
# alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:..."
hosts:
- host: my-recipes.aws-dvirlabs.com
paths:
- path: /
pathType: Prefix
externalUrl: "https://my-recipes.aws-dvirlabs.com"
# PostgreSQL configuration
postgres:
# For AWS RDS, set this to use external database
# Leave enabled: true to use in-cluster database
enabled: false # Set to false if using RDS
# If using RDS, these values are ignored but kept for reference
persistence:
storageClass: "gp3" # EKS default storage class
size: 20Gi
# OAuth Configuration
oauth:
google:
clientId: "143092846986-hsi59m0on2c9rb5qrdoejfceieao2ioc.apps.googleusercontent.com"
clientSecret: "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"
redirectUri: "https://api-my-recipes.aws-dvirlabs.com/auth/google/callback"
azure:
clientId: "db244cf5-eb11-4738-a2ea-5b0716c9ec0a"
clientSecret: "Zad8Q~qRBxaQq8up0lLXAq4pHzrVM2JFGFJhHaDp"
tenantId: "consumers"
redirectUri: "https://api-my-recipes.aws-dvirlabs.com/auth/azure/callback"
# Email Configuration
email:
smtpHost: "smtp.gmail.com"
smtpPort: "587"
smtpUser: "dvirlabs@gmail.com"
smtpPassword: "agaanrhbbazbdytv"
smtpFrom: "dvirlabs@gmail.com"
# S3 Backup Configuration for AWS
s3:
endpoint: "https://s3.eu-central-1.amazonaws.com" # Update with your region
accessKey: "AKIAXXXXXXXXXXXXXXXX" # Replace with your AWS Access Key
secretKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Replace with your AWS Secret Key
bucketName: "my-recipes-backups" # Update with your S3 bucket name
region: "eu-central-1" # Update with your region
backupInterval: "weekly"
# Admin User Configuration
admin:
username: "admin"
email: "dvirlabs@gmail.com"
password: "AdminPassword123!" # Change this after first login!
firstName: "Dvir"
lastName: "Admin"
displayName: "Dvir Admin"
# Database connection for AWS RDS (used when postgres.enabled: false)
database:
host: "my-recipes-rds.chw4omcguqv7.eu-central-1.rds.amazonaws.com"
port: "5432"
name: "recipes_db"
user: "recipes_user"
password: "recipes_password" # Store securely in AWS Secrets Manager in production

View File

@ -19,4 +19,4 @@ COPY . .
EXPOSE 8000 EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@ -93,9 +93,9 @@ def update_recipe_db(recipe_id: int, recipe_data: Dict[str, Any]) -> Optional[Di
recipe_data["name"], recipe_data["name"],
recipe_data["meal_type"], recipe_data["meal_type"],
recipe_data["time_minutes"], recipe_data["time_minutes"],
json.dumps(recipe_data.get("tags", [])), recipe_data.get("tags", []),
json.dumps(recipe_data.get("ingredients", [])), recipe_data.get("ingredients", []),
json.dumps(recipe_data.get("steps", [])), recipe_data.get("steps", []),
recipe_data.get("image"), recipe_data.get("image"),
recipe_data.get("made_by"), recipe_data.get("made_by"),
recipe_id, recipe_id,
@ -143,9 +143,9 @@ def create_recipe_db(recipe_data: Dict[str, Any]) -> Dict[str, Any]:
recipe_data["name"], recipe_data["name"],
recipe_data["meal_type"], recipe_data["meal_type"],
recipe_data["time_minutes"], recipe_data["time_minutes"],
json.dumps(recipe_data.get("tags", [])), recipe_data.get("tags", []),
json.dumps(recipe_data.get("ingredients", [])), recipe_data.get("ingredients", []),
json.dumps(recipe_data.get("steps", [])), recipe_data.get("steps", []),
recipe_data.get("image"), recipe_data.get("image"),
recipe_data.get("made_by"), recipe_data.get("made_by"),
recipe_data.get("user_id"), recipe_data.get("user_id"),

View File

@ -242,7 +242,7 @@ allowed_origins = [
"http://localhost:3000", "http://localhost:3000",
"https://my-recipes.dvirlabs.com", "https://my-recipes.dvirlabs.com",
"http://my-recipes.dvirlabs.com", "http://my-recipes.dvirlabs.com",
"http://my-recipes.aws-dvirlabs.com", "https://my-recipes.aws-dvirlabs.com"
] ]
app.add_middleware( app.add_middleware(
@ -1232,4 +1232,4 @@ def trigger_restore(
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True)

View File

@ -8,11 +8,13 @@ CREATE TABLE IF NOT EXISTS users (
last_name TEXT, last_name TEXT,
display_name TEXT UNIQUE NOT NULL, display_name TEXT UNIQUE NOT NULL,
is_admin BOOLEAN DEFAULT FALSE, is_admin BOOLEAN DEFAULT FALSE,
auth_provider VARCHAR(50) DEFAULT 'local' NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS idx_users_username ON users (username); CREATE INDEX IF NOT EXISTS idx_users_username ON users (username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email); CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
CREATE INDEX IF NOT EXISTS idx_users_auth_provider ON users (auth_provider);
-- Create recipes table -- Create recipes table
CREATE TABLE IF NOT EXISTS recipes ( CREATE TABLE IF NOT EXISTS recipes (
@ -20,13 +22,15 @@ CREATE TABLE IF NOT EXISTS recipes (
name TEXT NOT NULL, name TEXT NOT NULL,
meal_type TEXT NOT NULL, -- breakfast / lunch / dinner / snack meal_type TEXT NOT NULL, -- breakfast / lunch / dinner / snack
time_minutes INTEGER NOT NULL, time_minutes INTEGER NOT NULL,
tags TEXT[] NOT NULL DEFAULT '{}', -- {"מהיר", "בריא"} tags JSONB NOT NULL DEFAULT '[]', -- ["מהיר", "בריא"]
ingredients TEXT[] NOT NULL DEFAULT '{}', -- {"ביצה", "עגבניה", "מלח"} ingredients JSONB NOT NULL DEFAULT '[]', -- ["ביצה", "עגבניה", "מלח"]
steps TEXT[] NOT NULL DEFAULT '{}', -- {"לחתוך", "לבשל", ...} steps JSONB NOT NULL DEFAULT '[]', -- ["לחתוך", "לבשל", ...]
image TEXT, -- Base64-encoded image or image URL image TEXT, -- Base64-encoded image or image URL
made_by TEXT, -- Person who created this recipe version made_by TEXT, -- Person who created this recipe version
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, -- Recipe owner user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, -- Recipe owner
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP visibility VARCHAR(20) DEFAULT 'public', -- public / private / friends / groups
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT recipes_visibility_check CHECK (visibility IN ('public', 'private', 'friends', 'groups'))
); );
-- Optional: index for filters -- Optional: index for filters
@ -81,6 +85,148 @@ CREATE TABLE IF NOT EXISTS notifications (
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications (user_id); CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications (user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications (is_read); CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications (is_read);
-- Create friend requests table
CREATE TABLE IF NOT EXISTS friend_requests (
id SERIAL PRIMARY KEY,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
receiver_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT friend_requests_check CHECK (sender_id <> receiver_id),
CONSTRAINT friend_requests_status_check CHECK (status IN ('pending', 'accepted', 'rejected')),
UNIQUE(sender_id, receiver_id)
);
CREATE INDEX IF NOT EXISTS idx_friend_requests_sender_id ON friend_requests (sender_id);
CREATE INDEX IF NOT EXISTS idx_friend_requests_receiver_id ON friend_requests (receiver_id);
CREATE INDEX IF NOT EXISTS idx_friend_requests_status ON friend_requests (status);
-- Create friendships table
CREATE TABLE IF NOT EXISTS friendships (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
friend_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT friendships_check CHECK (user_id <> friend_id),
UNIQUE(user_id, friend_id)
);
CREATE INDEX IF NOT EXISTS idx_friendships_user_id ON friendships (user_id);
CREATE INDEX IF NOT EXISTS idx_friendships_friend_id ON friendships (friend_id);
-- Create groups table
CREATE TABLE IF NOT EXISTS groups (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
is_private BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_groups_created_by ON groups (created_by);
-- Create group members table
CREATE TABLE IF NOT EXISTS group_members (
id SERIAL PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) DEFAULT 'member',
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT group_members_role_check CHECK (role IN ('admin', 'moderator', 'member')),
UNIQUE(group_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_group_members_group_id ON group_members (group_id);
CREATE INDEX IF NOT EXISTS idx_group_members_user_id ON group_members (user_id);
-- Create conversations table
CREATE TABLE IF NOT EXISTS conversations (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
is_group BOOLEAN DEFAULT FALSE,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_conversations_created_by ON conversations (created_by);
-- Create conversation members table
CREATE TABLE IF NOT EXISTS conversation_members (
id SERIAL PRIMARY KEY,
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_read_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(conversation_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_conversation_members_conversation_id ON conversation_members (conversation_id);
CREATE INDEX IF NOT EXISTS idx_conversation_members_user_id ON conversation_members (user_id);
-- Create messages table
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
edited_at TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages (conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_sender_id ON messages (sender_id);
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages (created_at);
-- Create recipe shares table
CREATE TABLE IF NOT EXISTS recipe_shares (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
shared_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
shared_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(recipe_id, group_id)
);
CREATE INDEX IF NOT EXISTS idx_recipe_shares_recipe_id ON recipe_shares (recipe_id);
CREATE INDEX IF NOT EXISTS idx_recipe_shares_group_id ON recipe_shares (group_id);
CREATE INDEX IF NOT EXISTS idx_recipe_shares_shared_by ON recipe_shares (shared_by);
-- Create recipe ratings table
CREATE TABLE IF NOT EXISTS recipe_ratings (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rating INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT recipe_ratings_rating_check CHECK (rating >= 1 AND rating <= 5),
UNIQUE(recipe_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_recipe_ratings_recipe_id ON recipe_ratings (recipe_id);
CREATE INDEX IF NOT EXISTS idx_recipe_ratings_user_id ON recipe_ratings (user_id);
-- Create recipe comments table
CREATE TABLE IF NOT EXISTS recipe_comments (
id SERIAL PRIMARY KEY,
recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
parent_comment_id INTEGER REFERENCES recipe_comments(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_recipe_comments_recipe_id ON recipe_comments (recipe_id);
CREATE INDEX IF NOT EXISTS idx_recipe_comments_user_id ON recipe_comments (user_id);
CREATE INDEX IF NOT EXISTS idx_recipe_comments_parent_comment_id ON recipe_comments (parent_comment_id);
-- Create default admin user (password: admin123) -- Create default admin user (password: admin123)
-- Password hash generated with bcrypt for 'admin123' -- Password hash generated with bcrypt for 'admin123'
INSERT INTO users (username, email, password_hash, first_name, last_name, display_name, is_admin) INSERT INTO users (username, email, password_hash, first_name, last_name, display_name, is_admin)