Update cors origin
This commit is contained in:
commit
abc9e94104
13
aws/.env
13
aws/.env
@ -2,7 +2,7 @@
|
||||
DB_USER=recipes_user
|
||||
DB_PASSWORD=recipes_password
|
||||
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
|
||||
|
||||
# Email Configuration
|
||||
@ -24,11 +24,12 @@ AZURE_CLIENT_SECRET=Zad8Q~qRBxaQq8up0lLXAq4pHzrVM2JFGFJhHaDp
|
||||
AZURE_TENANT_ID=consumers
|
||||
AZURE_REDIRECT_URI=http://localhost:8000/auth/azure/callback
|
||||
|
||||
# Cloudflare R2 Backup Configuration
|
||||
R2_ENDPOINT=https://d4704b8c40b2f95b2c7bf7ee4ecc52f8.r2.cloudflarestorage.com
|
||||
R2_ACCESS_KEY=1997b1e48a337c0dbe1f7552a08631b5
|
||||
R2_SECRET_KEY=369694e39fedfedb254158c147171f5760de84fa2346d5d5d5a961f1f517dbc6
|
||||
R2_BUCKET_NAME=recipes-backups
|
||||
# AWS S3 Backup Configuration
|
||||
S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
|
||||
S3_ACCESS_KEY=1997b1e48a337c0dbe1f7552a08631b5
|
||||
S3_SECRET_KEY=369694e39fedfedb254158c147171f5760de84fa2346d5d5d5a961f1f517dbc6
|
||||
S3_BUCKET_NAME=recipes-backups
|
||||
S3_REGION=eu-central-1
|
||||
|
||||
# Automatic Backup Schedule
|
||||
# Options: test (every 1 minute), daily, weekly, disabled
|
||||
|
||||
225
aws/EKS_DEPLOYMENT.md
Normal file
225
aws/EKS_DEPLOYMENT.md
Normal 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
|
||||
14
aws/final-app/my-recipes-chart-aws/Chart.yaml
Normal file
14
aws/final-app/my-recipes-chart-aws/Chart.yaml
Normal 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
|
||||
|
||||
@ -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);
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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 }}
|
||||
@ -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
|
||||
|
||||
@ -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 }}
|
||||
@ -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;
|
||||
@ -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
|
||||
@ -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);
|
||||
23
aws/final-app/my-recipes-chart-aws/templates/db-secret.yaml
Normal file
23
aws/final-app/my-recipes-chart-aws/templates/db-secret.yaml
Normal 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 }}
|
||||
|
||||
39
aws/final-app/my-recipes-chart-aws/templates/db-service.yaml
Normal file
39
aws/final-app/my-recipes-chart-aws/templates/db-service.yaml
Normal 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 }}
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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 }}
|
||||
@ -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 }}
|
||||
48
aws/final-app/my-recipes-chart-aws/templates/ingress.yaml
Normal file
48
aws/final-app/my-recipes-chart-aws/templates/ingress.yaml
Normal 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 }}
|
||||
182
aws/final-app/my-recipes-chart-aws/values.yaml
Normal file
182
aws/final-app/my-recipes-chart-aws/values.yaml
Normal 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
110
aws/final-app/values.yaml
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -93,9 +93,9 @@ def update_recipe_db(recipe_id: int, recipe_data: Dict[str, Any]) -> Optional[Di
|
||||
recipe_data["name"],
|
||||
recipe_data["meal_type"],
|
||||
recipe_data["time_minutes"],
|
||||
json.dumps(recipe_data.get("tags", [])),
|
||||
json.dumps(recipe_data.get("ingredients", [])),
|
||||
json.dumps(recipe_data.get("steps", [])),
|
||||
recipe_data.get("tags", []),
|
||||
recipe_data.get("ingredients", []),
|
||||
recipe_data.get("steps", []),
|
||||
recipe_data.get("image"),
|
||||
recipe_data.get("made_by"),
|
||||
recipe_id,
|
||||
@ -143,9 +143,9 @@ def create_recipe_db(recipe_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
recipe_data["name"],
|
||||
recipe_data["meal_type"],
|
||||
recipe_data["time_minutes"],
|
||||
json.dumps(recipe_data.get("tags", [])),
|
||||
json.dumps(recipe_data.get("ingredients", [])),
|
||||
json.dumps(recipe_data.get("steps", [])),
|
||||
recipe_data.get("tags", []),
|
||||
recipe_data.get("ingredients", []),
|
||||
recipe_data.get("steps", []),
|
||||
recipe_data.get("image"),
|
||||
recipe_data.get("made_by"),
|
||||
recipe_data.get("user_id"),
|
||||
|
||||
@ -242,7 +242,7 @@ allowed_origins = [
|
||||
"http://localhost:3000",
|
||||
"https://my-recipes.dvirlabs.com",
|
||||
"http://my-recipes.dvirlabs.com",
|
||||
"http://my-recipes.aws-dvirlabs.com",
|
||||
"https://my-recipes.aws-dvirlabs.com"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
@ -1232,4 +1232,4 @@ def trigger_restore(
|
||||
|
||||
|
||||
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)
|
||||
@ -8,11 +8,13 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
last_name TEXT,
|
||||
display_name TEXT UNIQUE NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
auth_provider VARCHAR(50) DEFAULT 'local' NOT NULL,
|
||||
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 INDEX IF NOT EXISTS idx_users_auth_provider ON users (auth_provider);
|
||||
|
||||
-- Create recipes table
|
||||
CREATE TABLE IF NOT EXISTS recipes (
|
||||
@ -20,13 +22,15 @@ CREATE TABLE IF NOT EXISTS recipes (
|
||||
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 '{}', -- {"לחתוך", "לבשל", ...}
|
||||
tags JSONB NOT NULL DEFAULT '[]', -- ["מהיר", "בריא"]
|
||||
ingredients JSONB NOT NULL DEFAULT '[]', -- ["ביצה", "עגבניה", "מלח"]
|
||||
steps JSONB 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
|
||||
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
|
||||
@ -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_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)
|
||||
-- Password hash generated with bcrypt for 'admin123'
|
||||
INSERT INTO users (username, email, password_hash, first_name, last_name, display_name, is_admin)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user