Add dev/
This commit is contained in:
parent
deaa10181a
commit
4fc279e17c
@ -100,6 +100,9 @@ steps:
|
|||||||
|
|
||||||
|
|
||||||
trigger-gitops-via-push:
|
trigger-gitops-via-push:
|
||||||
|
when:
|
||||||
|
branch: [ master, develop ]
|
||||||
|
event: [ push ]
|
||||||
name: Trigger apps-gitops via Git push
|
name: Trigger apps-gitops via Git push
|
||||||
image: alpine/git
|
image: alpine/git
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
10
backend/.env
10
backend/.env
@ -2,3 +2,13 @@ MINIO_ACCESS_KEY=TDJvsBmbkpUXpCw5M7LA
|
|||||||
MINIO_SECRET_KEY=n9scR7W0MZy6FF0bznV98fSgXpdebIQjqZvEr1Yu
|
MINIO_SECRET_KEY=n9scR7W0MZy6FF0bznV98fSgXpdebIQjqZvEr1Yu
|
||||||
MINIO_ENDPOINT=s3.dvirlabs.com
|
MINIO_ENDPOINT=s3.dvirlabs.com
|
||||||
MINIO_BUCKET=navix-icons
|
MINIO_BUCKET=navix-icons
|
||||||
|
|
||||||
|
# PostgreSQL Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=navix
|
||||||
|
DB_USER=navix_user
|
||||||
|
DB_PASSWORD=Aa123456
|
||||||
|
|
||||||
|
# JWT Authentication
|
||||||
|
JWT_SECRET_KEY=9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e
|
||||||
|
|||||||
15
backend/.env.example
Normal file
15
backend/.env.example
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# MinIO Configuration
|
||||||
|
MINIO_ENDPOINT=s3.dvirlabs.com
|
||||||
|
MINIO_ACCESS_KEY=your-access-key
|
||||||
|
MINIO_SECRET_KEY=your-secret-key
|
||||||
|
MINIO_BUCKET=navix-icons
|
||||||
|
|
||||||
|
# PostgreSQL Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=navix
|
||||||
|
DB_USER=navix_user
|
||||||
|
DB_PASSWORD=navix_secure_password_change_me
|
||||||
|
|
||||||
|
# JWT Authentication
|
||||||
|
JWT_SECRET_KEY=your-super-secret-jwt-key-change-this-in-production-min-32-chars
|
||||||
135
backend/README.md
Normal file
135
backend/README.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# Navix Backend - PostgreSQL Setup Guide
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Install PostgreSQL
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```bash
|
||||||
|
# Download from https://www.postgresql.org/download/windows/
|
||||||
|
# Or use chocolatey:
|
||||||
|
choco install postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/WSL:**
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install postgresql postgresql-contrib
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew install postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start PostgreSQL Service
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```bash
|
||||||
|
# PostgreSQL should start automatically as a service
|
||||||
|
# Or start manually:
|
||||||
|
pg_ctl -D "C:\Program Files\PostgreSQL\{version}\data" start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/WSL:**
|
||||||
|
```bash
|
||||||
|
sudo service postgresql start
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
brew services start postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create Database and Schema
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the schema file as postgres superuser
|
||||||
|
psql -U postgres -f backend/schema.sql
|
||||||
|
|
||||||
|
# This will:
|
||||||
|
# 1. Create a dedicated 'navix_user' database user
|
||||||
|
# 2. Create the 'navix' database owned by navix_user
|
||||||
|
# 3. Create all tables and schemas
|
||||||
|
# 4. Grant appropriate privileges
|
||||||
|
|
||||||
|
# IMPORTANT: Change the password in the SQL file before running!
|
||||||
|
# Edit backend/schema.sql and change 'navix_secure_password_change_me' to a strong password
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configure Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and update with your settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` with your actual database credentials and JWT secret key.
|
||||||
|
|
||||||
|
### 5. Install Python Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Run the Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/auth/register` - Register new user
|
||||||
|
- `POST /api/auth/login` - Login and get JWT token
|
||||||
|
- `GET /api/auth/me` - Get current user info (requires auth)
|
||||||
|
|
||||||
|
### Sections
|
||||||
|
- `GET /api/sections` - Get all sections with apps (requires auth)
|
||||||
|
- `POST /api/sections` - Create new section (requires auth)
|
||||||
|
|
||||||
|
### Apps
|
||||||
|
- `POST /api/apps` - Create new app (requires auth)
|
||||||
|
- `PUT /api/apps/{app_id}` - Update app (requires auth)
|
||||||
|
- `DELETE /api/apps/{app_id}` - Delete app (requires auth)
|
||||||
|
|
||||||
|
### Legacy (YAML-based)
|
||||||
|
- `GET /api/apps` - Get apps from YAML file
|
||||||
|
- `POST /api/add_app` - Add app to YAML file
|
||||||
|
- `POST /api/edit_app` - Edit app in YAML file
|
||||||
|
- `POST /api/delete_app` - Delete app from YAML file
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
1. **Register**: `POST /api/auth/register` with `{username, email, password}`
|
||||||
|
2. **Login**: `POST /api/auth/login` with `{username, password}` → Returns JWT token
|
||||||
|
3. **Use Token**: Include in Authorization header: `Bearer {token}`
|
||||||
|
|
||||||
|
## Testing with cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Register
|
||||||
|
curl -X POST http://localhost:8000/api/auth/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"testuser","email":"test@example.com","password":"password123"}'
|
||||||
|
|
||||||
|
# Login
|
||||||
|
curl -X POST http://localhost:8000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"testuser","password":"password123"}'
|
||||||
|
|
||||||
|
# Get sections (use token from login)
|
||||||
|
curl -X GET http://localhost:8000/api/sections \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
- **users**: User accounts with authentication
|
||||||
|
- **sections**: User-specific app sections
|
||||||
|
- **apps**: Applications within sections
|
||||||
|
|
||||||
|
Each user has their own personal view with their own sections and apps.
|
||||||
BIN
backend/__pycache__/auth.cpython-313.pyc
Normal file
BIN
backend/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
@ -45,3 +45,15 @@ sections:
|
|||||||
name: Minio
|
name: Minio
|
||||||
url: https://minio.dvirlabs.com
|
url: https://minio.dvirlabs.com
|
||||||
name: Infra
|
name: Infra
|
||||||
|
- apps:
|
||||||
|
- description: dvir
|
||||||
|
icon: ''
|
||||||
|
name: test-app
|
||||||
|
url: https://git.dvirlabs.com
|
||||||
|
name: test
|
||||||
|
- apps:
|
||||||
|
- description: sdf
|
||||||
|
icon: ''
|
||||||
|
name: dsfds
|
||||||
|
url: https://git.dvirlabs.com
|
||||||
|
name: sdf
|
||||||
|
|||||||
81
backend/auth.py
Normal file
81
backend/auth.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
import bcrypt
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
import os
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-this-in-production")
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
|
||||||
|
|
||||||
|
# HTTP Bearer token scheme
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""Verify a password against its hash"""
|
||||||
|
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""Hash a password using bcrypt"""
|
||||||
|
salt = bcrypt.gensalt()
|
||||||
|
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||||
|
return hashed.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||||
|
"""Create a JWT access token"""
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def decode_access_token(token: str) -> dict:
|
||||||
|
"""Decode and verify a JWT token"""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
return payload
|
||||||
|
except JWTError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
|
||||||
|
"""Dependency to get the current authenticated user from JWT token"""
|
||||||
|
try:
|
||||||
|
token = credentials.credentials
|
||||||
|
payload = decode_access_token(token)
|
||||||
|
|
||||||
|
user_id = payload.get("sub")
|
||||||
|
if user_id is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials - no user_id",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to int if it's a string
|
||||||
|
if isinstance(user_id, str):
|
||||||
|
user_id = int(user_id)
|
||||||
|
|
||||||
|
return {"user_id": user_id, "username": payload.get("username")}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Auth error: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=f"Could not validate credentials: {str(e)}",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
56
backend/database.py
Normal file
56
backend/database.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
# Database configuration from environment variables
|
||||||
|
DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||||
|
DB_PORT = os.getenv("DB_PORT", "5432")
|
||||||
|
DB_NAME = os.getenv("DB_NAME", "navix")
|
||||||
|
DB_USER = os.getenv("DB_USER", "postgres")
|
||||||
|
DB_PASSWORD = os.getenv("DB_PASSWORD", "postgres")
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
"""Create a new database connection"""
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
database=DB_NAME,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
cursor_factory=RealDictCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_db_cursor(commit=False):
|
||||||
|
"""Context manager for database operations"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
yield cursor
|
||||||
|
if commit:
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Test database connection"""
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT version();")
|
||||||
|
version = cursor.fetchone()
|
||||||
|
print(f"Connected to PostgreSQL: {version}")
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Database connection failed: {e}")
|
||||||
|
return False
|
||||||
322
backend/main.py
322
backend/main.py
@ -1,4 +1,4 @@
|
|||||||
from fastapi import FastAPI, APIRouter
|
from fastapi import FastAPI, APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@ -6,10 +6,32 @@ import os
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import yaml
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pydantic import BaseModel
|
|
||||||
from minio import Minio
|
from minio import Minio
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
app = FastAPI()
|
from database import get_db_cursor, init_db
|
||||||
|
from auth import (
|
||||||
|
get_password_hash,
|
||||||
|
verify_password,
|
||||||
|
create_access_token,
|
||||||
|
get_current_user,
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
from models import (
|
||||||
|
UserRegister,
|
||||||
|
UserLogin,
|
||||||
|
Token,
|
||||||
|
UserResponse,
|
||||||
|
SectionCreate,
|
||||||
|
SectionResponse,
|
||||||
|
AppCreate,
|
||||||
|
AppUpdate,
|
||||||
|
AppResponse,
|
||||||
|
AppEntry,
|
||||||
|
AppData
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI(title="Navix API", version="2.0.0")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@ -36,35 +58,292 @@ minio_client = Minio(
|
|||||||
BUCKET = MINIO_BUCKET or "navix-icons"
|
BUCKET = MINIO_BUCKET or "navix-icons"
|
||||||
APPS_FILE = Path(__file__).parent / "apps.yaml"
|
APPS_FILE = Path(__file__).parent / "apps.yaml"
|
||||||
|
|
||||||
|
# Initialize database connection
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
if init_db():
|
||||||
|
print("✅ Database connected successfully")
|
||||||
|
else:
|
||||||
|
print("⚠️ Database connection failed")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Authentication Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.post("/auth/register", response_model=Token, status_code=status.HTTP_201_CREATED)
|
||||||
|
def register(user_data: UserRegister):
|
||||||
|
"""Register a new user"""
|
||||||
|
with get_db_cursor(commit=True) as cursor:
|
||||||
|
# Check if username exists
|
||||||
|
cursor.execute("SELECT id FROM users WHERE username = %s", (user_data.username,))
|
||||||
|
if cursor.fetchone():
|
||||||
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
|
||||||
|
# Check if email exists
|
||||||
|
cursor.execute("SELECT id FROM users WHERE email = %s", (user_data.email,))
|
||||||
|
if cursor.fetchone():
|
||||||
|
raise HTTPException(status_code=400, detail="Email already exists")
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
hashed_password = get_password_hash(user_data.password)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (username, email, password_hash)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
RETURNING id, username, email, created_at
|
||||||
|
""",
|
||||||
|
(user_data.username, user_data.email, hashed_password)
|
||||||
|
)
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
# Create access token
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user["id"], "username": user["username"]},
|
||||||
|
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Token(
|
||||||
|
access_token=access_token,
|
||||||
|
user=UserResponse(**user)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/auth/login", response_model=Token)
|
||||||
|
def login(credentials: UserLogin):
|
||||||
|
"""Login user and return JWT token"""
|
||||||
|
with get_db_cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id, username, email, password_hash, created_at FROM users WHERE username = %s",
|
||||||
|
(credentials.username,)
|
||||||
|
)
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
if not user or not verify_password(credentials.password, user["password_hash"]):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create access token
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user["id"], "username": user["username"]},
|
||||||
|
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Token(
|
||||||
|
access_token=access_token,
|
||||||
|
user=UserResponse(
|
||||||
|
id=user["id"],
|
||||||
|
username=user["username"],
|
||||||
|
email=user["email"],
|
||||||
|
created_at=user["created_at"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/auth/me", response_model=UserResponse)
|
||||||
|
def get_me(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get current user information"""
|
||||||
|
with get_db_cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id, username, email, created_at FROM users WHERE id = %s",
|
||||||
|
(current_user["user_id"],)
|
||||||
|
)
|
||||||
|
user = cursor.fetchone()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
return UserResponse(**user)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Section Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.get("/sections", response_model=list[SectionResponse])
|
||||||
|
def get_sections(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get all sections with apps for the current user"""
|
||||||
|
with get_db_cursor() as cursor:
|
||||||
|
# Get sections for user
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, name, display_order
|
||||||
|
FROM sections
|
||||||
|
WHERE user_id = %s
|
||||||
|
ORDER BY display_order, name
|
||||||
|
""",
|
||||||
|
(current_user["user_id"],)
|
||||||
|
)
|
||||||
|
sections = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for section in sections:
|
||||||
|
# Get apps for each section
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, section_id, name, url, icon, description, display_order
|
||||||
|
FROM apps
|
||||||
|
WHERE section_id = %s
|
||||||
|
ORDER BY display_order, name
|
||||||
|
""",
|
||||||
|
(section["id"],)
|
||||||
|
)
|
||||||
|
apps = cursor.fetchall()
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"id": section["id"],
|
||||||
|
"name": section["name"],
|
||||||
|
"display_order": section["display_order"],
|
||||||
|
"apps": [dict(app) for app in apps]
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sections", response_model=SectionResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_section(section: SectionCreate, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Create a new section for the current user"""
|
||||||
|
with get_db_cursor(commit=True) as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO sections (user_id, name, display_order)
|
||||||
|
VALUES (%s, %s, COALESCE((SELECT MAX(display_order) + 1 FROM sections WHERE user_id = %s), 0))
|
||||||
|
RETURNING id, name, display_order
|
||||||
|
""",
|
||||||
|
(current_user["user_id"], section.name, current_user["user_id"])
|
||||||
|
)
|
||||||
|
new_section = cursor.fetchone()
|
||||||
|
return SectionResponse(**new_section, apps=[])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# App Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@router.post("/apps", response_model=AppResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_app(app: AppCreate, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Create a new app in a section"""
|
||||||
|
with get_db_cursor(commit=True) as cursor:
|
||||||
|
# Verify section belongs to user
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM sections WHERE id = %s AND user_id = %s",
|
||||||
|
(app.section_id, current_user["user_id"])
|
||||||
|
)
|
||||||
|
if not cursor.fetchone():
|
||||||
|
raise HTTPException(status_code=404, detail="Section not found")
|
||||||
|
|
||||||
|
# Create app
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO apps (section_id, name, url, icon, description, display_order)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, COALESCE((SELECT MAX(display_order) + 1 FROM apps WHERE section_id = %s), 0))
|
||||||
|
RETURNING id, section_id, name, url, icon, description, display_order
|
||||||
|
""",
|
||||||
|
(app.section_id, app.name, app.url, app.icon, app.description, app.section_id)
|
||||||
|
)
|
||||||
|
new_app = cursor.fetchone()
|
||||||
|
return AppResponse(**new_app)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/apps/{app_id}", response_model=AppResponse)
|
||||||
|
def update_app(app_id: int, app_update: AppUpdate, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Update an existing app"""
|
||||||
|
with get_db_cursor(commit=True) as cursor:
|
||||||
|
# Verify app belongs to user
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT a.id FROM apps a
|
||||||
|
JOIN sections s ON a.section_id = s.id
|
||||||
|
WHERE a.id = %s AND s.user_id = %s
|
||||||
|
""",
|
||||||
|
(app_id, current_user["user_id"])
|
||||||
|
)
|
||||||
|
if not cursor.fetchone():
|
||||||
|
raise HTTPException(status_code=404, detail="App not found")
|
||||||
|
|
||||||
|
# Build update query dynamically
|
||||||
|
update_fields = []
|
||||||
|
values = []
|
||||||
|
|
||||||
|
if app_update.name is not None:
|
||||||
|
update_fields.append("name = %s")
|
||||||
|
values.append(app_update.name)
|
||||||
|
if app_update.url is not None:
|
||||||
|
update_fields.append("url = %s")
|
||||||
|
values.append(app_update.url)
|
||||||
|
if app_update.icon is not None:
|
||||||
|
update_fields.append("icon = %s")
|
||||||
|
values.append(app_update.icon)
|
||||||
|
if app_update.description is not None:
|
||||||
|
update_fields.append("description = %s")
|
||||||
|
values.append(app_update.description)
|
||||||
|
if app_update.section_id is not None:
|
||||||
|
# Verify new section belongs to user
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id FROM sections WHERE id = %s AND user_id = %s",
|
||||||
|
(app_update.section_id, current_user["user_id"])
|
||||||
|
)
|
||||||
|
if not cursor.fetchone():
|
||||||
|
raise HTTPException(status_code=404, detail="Target section not found")
|
||||||
|
update_fields.append("section_id = %s")
|
||||||
|
values.append(app_update.section_id)
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
raise HTTPException(status_code=400, detail="No fields to update")
|
||||||
|
|
||||||
|
values.append(app_id)
|
||||||
|
query = f"UPDATE apps SET {', '.join(update_fields)} WHERE id = %s RETURNING id, section_id, name, url, icon, description, display_order"
|
||||||
|
|
||||||
|
cursor.execute(query, values)
|
||||||
|
updated_app = cursor.fetchone()
|
||||||
|
return AppResponse(**updated_app)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/apps/{app_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def delete_app_by_id(app_id: int, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Delete an app"""
|
||||||
|
with get_db_cursor(commit=True) as cursor:
|
||||||
|
# Verify app belongs to user
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM apps
|
||||||
|
WHERE id = %s AND section_id IN (
|
||||||
|
SELECT id FROM sections WHERE user_id = %s
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
(app_id, current_user["user_id"])
|
||||||
|
)
|
||||||
|
if not cursor.fetchone():
|
||||||
|
raise HTTPException(status_code=404, detail="App not found")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Legacy YAML Endpoints (for backward compatibility)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Legacy YAML Endpoints (for backward compatibility)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {"message": "Welcome to the FastAPI application!"}
|
return {"message": "Welcome to Navix API v2.0!"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/apps")
|
@router.get("/apps")
|
||||||
def get_apps():
|
def get_apps():
|
||||||
|
"""Legacy endpoint - returns apps from YAML file"""
|
||||||
if not APPS_FILE.exists():
|
if not APPS_FILE.exists():
|
||||||
return {"error": "apps.yaml not found"}
|
return {"error": "apps.yaml not found"}
|
||||||
with open(APPS_FILE, "r") as f:
|
with open(APPS_FILE, "r") as f:
|
||||||
return yaml.safe_load(f)
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
class AppData(BaseModel):
|
|
||||||
name: str
|
|
||||||
icon: str
|
|
||||||
description: str
|
|
||||||
url: str
|
|
||||||
|
|
||||||
|
|
||||||
class AppEntry(BaseModel):
|
|
||||||
section: str
|
|
||||||
app: AppData
|
|
||||||
original_name: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/add_app")
|
@router.post("/add_app")
|
||||||
def add_app(entry: AppEntry):
|
def add_app(entry: AppEntry):
|
||||||
|
"""Legacy endpoint - adds app to YAML file"""
|
||||||
if not APPS_FILE.exists():
|
if not APPS_FILE.exists():
|
||||||
current = {"sections": []}
|
current = {"sections": []}
|
||||||
else:
|
else:
|
||||||
@ -89,6 +368,7 @@ def add_app(entry: AppEntry):
|
|||||||
|
|
||||||
@router.post("/edit_app")
|
@router.post("/edit_app")
|
||||||
def edit_app(entry: AppEntry):
|
def edit_app(entry: AppEntry):
|
||||||
|
"""Legacy endpoint - edits app in YAML file"""
|
||||||
if not APPS_FILE.exists():
|
if not APPS_FILE.exists():
|
||||||
return {"error": "apps.yaml not found"}
|
return {"error": "apps.yaml not found"}
|
||||||
|
|
||||||
@ -116,6 +396,7 @@ def edit_app(entry: AppEntry):
|
|||||||
|
|
||||||
@router.post("/delete_app")
|
@router.post("/delete_app")
|
||||||
def delete_app(entry: AppEntry):
|
def delete_app(entry: AppEntry):
|
||||||
|
"""Legacy endpoint - deletes app from YAML file"""
|
||||||
if not APPS_FILE.exists():
|
if not APPS_FILE.exists():
|
||||||
return {"error": "apps.yaml not found"}
|
return {"error": "apps.yaml not found"}
|
||||||
|
|
||||||
@ -142,13 +423,18 @@ def delete_app(entry: AppEntry):
|
|||||||
|
|
||||||
@router.get("/icon/{filename}")
|
@router.get("/icon/{filename}")
|
||||||
def get_public_icon_url(filename: str):
|
def get_public_icon_url(filename: str):
|
||||||
|
"""Get public URL for an icon from MinIO"""
|
||||||
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
|
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
|
||||||
return JSONResponse(content={"url": url})
|
return JSONResponse(content={"url": url})
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# App Registration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
app.include_router(router, prefix="/api")
|
app.include_router(router, prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
# ✅ This is the missing part:
|
|
||||||
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=8000, reload=True)
|
||||||
|
|
||||||
|
|||||||
77
backend/models.py
Normal file
77
backend/models.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from pydantic import BaseModel, EmailStr, Field
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegister(BaseModel):
|
||||||
|
username: str = Field(..., min_length=3, max_length=50)
|
||||||
|
email: EmailStr
|
||||||
|
password: str = Field(..., min_length=6)
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
email: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
user: UserResponse
|
||||||
|
|
||||||
|
|
||||||
|
class SectionCreate(BaseModel):
|
||||||
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
|
||||||
|
|
||||||
|
class SectionResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
display_order: int
|
||||||
|
apps: list = []
|
||||||
|
|
||||||
|
|
||||||
|
class AppCreate(BaseModel):
|
||||||
|
section_id: int
|
||||||
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
url: str
|
||||||
|
icon: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AppUpdate(BaseModel):
|
||||||
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||||
|
url: Optional[str] = None
|
||||||
|
icon: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
section_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AppResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
section_id: int
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
icon: Optional[str]
|
||||||
|
description: Optional[str]
|
||||||
|
display_order: int
|
||||||
|
|
||||||
|
|
||||||
|
class AppData(BaseModel):
|
||||||
|
name: str
|
||||||
|
icon: str
|
||||||
|
description: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
class AppEntry(BaseModel):
|
||||||
|
section: str
|
||||||
|
app: AppData
|
||||||
|
original_name: str | None = None
|
||||||
@ -36,6 +36,9 @@ pycparser==2.22
|
|||||||
pycryptodome==3.23.0
|
pycryptodome==3.23.0
|
||||||
pydantic==2.8.0
|
pydantic==2.8.0
|
||||||
pydantic_core==2.20.0
|
pydantic_core==2.20.0
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
PyGithub==2.3.0
|
PyGithub==2.3.0
|
||||||
Pygments==2.18.0
|
Pygments==2.18.0
|
||||||
PyJWT==2.8.0
|
PyJWT==2.8.0
|
||||||
|
|||||||
75
backend/schema.sql
Normal file
75
backend/schema.sql
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- Create dedicated user for navix application
|
||||||
|
CREATE USER navix_user WITH PASSWORD 'Aa123456';
|
||||||
|
|
||||||
|
-- Create database
|
||||||
|
CREATE DATABASE navix OWNER navix_user;
|
||||||
|
|
||||||
|
-- Grant privileges
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE navix TO navix_user;
|
||||||
|
|
||||||
|
-- Connect to navix database
|
||||||
|
\c navix;
|
||||||
|
|
||||||
|
-- Grant schema privileges
|
||||||
|
GRANT ALL ON SCHEMA public TO navix_user;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO navix_user;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO navix_user;
|
||||||
|
|
||||||
|
-- Create users table
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create sections table
|
||||||
|
CREATE TABLE sections (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(user_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create apps table
|
||||||
|
CREATE TABLE apps (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
section_id INTEGER NOT NULL REFERENCES sections(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
icon VARCHAR(255),
|
||||||
|
description TEXT,
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better query performance
|
||||||
|
CREATE INDEX idx_sections_user_id ON sections(user_id);
|
||||||
|
CREATE INDEX idx_apps_section_id ON apps(section_id);
|
||||||
|
CREATE INDEX idx_users_username ON users(username);
|
||||||
|
CREATE INDEX idx_users_email ON users(email);
|
||||||
|
|
||||||
|
-- Create updated_at trigger function
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
-- Add triggers for updated_at
|
||||||
|
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_apps_updated_at BEFORE UPDATE ON apps
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Grant privileges on all objects to navix_user
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO navix_user;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO navix_user;
|
||||||
@ -27,6 +27,45 @@ body {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* User info and logout */
|
||||||
|
.user-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5rem;
|
||||||
|
left: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
background: rgba(255, 59, 48, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button:hover {
|
||||||
|
background: rgba(255, 59, 48, 1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
/* 🔹 מרכז כותרת */
|
/* 🔹 מרכז כותרת */
|
||||||
.title-wrapper {
|
.title-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import AppModal from './components/AppModal';
|
|||||||
import ConfirmDialog from './components/ConfirmDialog';
|
import ConfirmDialog from './components/ConfirmDialog';
|
||||||
import Clock from './components/Clock';
|
import Clock from './components/Clock';
|
||||||
import Calendar from './components/Calendar';
|
import Calendar from './components/Calendar';
|
||||||
|
import Login from './components/Login';
|
||||||
|
import Register from './components/Register';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { IoIosAdd } from 'react-icons/io';
|
import { IoIosAdd, IoMdLogOut } from 'react-icons/io';
|
||||||
import CustomToast from './components/CustomToast';
|
import CustomToast from './components/CustomToast';
|
||||||
import {
|
import {
|
||||||
fetchSections,
|
fetchSections,
|
||||||
@ -15,6 +17,7 @@ import {
|
|||||||
editAppInSection,
|
editAppInSection,
|
||||||
deleteAppFromSection,
|
deleteAppFromSection,
|
||||||
} from './services/api';
|
} from './services/api';
|
||||||
|
import { isAuthenticated, logout, getUser } from './services/auth';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [sections, setSections] = useState([]);
|
const [sections, setSections] = useState([]);
|
||||||
@ -22,19 +25,54 @@ function App() {
|
|||||||
const [showAdd, setShowAdd] = useState(false);
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
const [confirmData, setConfirmData] = useState(null);
|
const [confirmData, setConfirmData] = useState(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [authenticated, setAuthenticated] = useState(isAuthenticated());
|
||||||
|
const [showRegister, setShowRegister] = useState(false);
|
||||||
|
const [user, setUser] = useState(getUser());
|
||||||
|
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
setAuthenticated(true);
|
||||||
|
setUser(getUser());
|
||||||
|
toast.success('Welcome back!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegisterSuccess = () => {
|
||||||
|
setAuthenticated(true);
|
||||||
|
setUser(getUser());
|
||||||
|
toast.success('Account created successfully!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
setAuthenticated(false);
|
||||||
|
setUser(null);
|
||||||
|
setSections([]);
|
||||||
|
toast.info('Logged out successfully');
|
||||||
|
};
|
||||||
|
|
||||||
const loadSections = () => {
|
const loadSections = () => {
|
||||||
|
if (!authenticated) return;
|
||||||
|
|
||||||
fetchSections()
|
fetchSections()
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const filtered = (data.sections || []).filter(section => (section.apps?.length ?? 0) > 0);
|
// Handle both old format {sections: []} and new format []
|
||||||
|
const sectionsArray = Array.isArray(data) ? data : (data.sections || []);
|
||||||
|
const filtered = sectionsArray.filter(section => (section.apps?.length ?? 0) > 0);
|
||||||
setSections(filtered);
|
setSections(filtered);
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Failed to fetch sections:', err));
|
.catch(err => {
|
||||||
|
console.error('Failed to fetch sections:', err);
|
||||||
|
// If unauthorized, might need to re-login
|
||||||
|
if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (authenticated) {
|
||||||
loadSections();
|
loadSections();
|
||||||
}, []);
|
}
|
||||||
|
}, [authenticated]);
|
||||||
|
|
||||||
const handleDelete = (app) => {
|
const handleDelete = (app) => {
|
||||||
setConfirmData({
|
setConfirmData({
|
||||||
@ -96,6 +134,14 @@ function App() {
|
|||||||
),
|
),
|
||||||
})).filter(section => section.apps.length > 0);
|
})).filter(section => section.apps.length > 0);
|
||||||
|
|
||||||
|
// Show login/register if not authenticated
|
||||||
|
if (!authenticated) {
|
||||||
|
if (showRegister) {
|
||||||
|
return <Register onRegisterSuccess={handleRegisterSuccess} onSwitchToLogin={() => setShowRegister(false)} />;
|
||||||
|
}
|
||||||
|
return <Login onLoginSuccess={handleLoginSuccess} onSwitchToRegister={() => setShowRegister(true)} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<div className="top-right-container">
|
<div className="top-right-container">
|
||||||
@ -103,6 +149,14 @@ function App() {
|
|||||||
<Clock />
|
<Clock />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* User info and logout button */}
|
||||||
|
<div className="user-info">
|
||||||
|
<span className="username">👤 {user?.username}</span>
|
||||||
|
<button className="logout-button" onClick={handleLogout} title="Logout">
|
||||||
|
<IoMdLogOut size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 🔹 לוגו וכותרת במרכז */}
|
{/* 🔹 לוגו וכותרת במרכז */}
|
||||||
<div className="title-wrapper">
|
<div className="title-wrapper">
|
||||||
<h1 className="main-title">
|
<h1 className="main-title">
|
||||||
|
|||||||
85
frontend/src/components/Login.jsx
Normal file
85
frontend/src/components/Login.jsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { login } from '../services/auth';
|
||||||
|
import '../style/Auth.css';
|
||||||
|
|
||||||
|
function Login({ onLoginSuccess, onSwitchToRegister }) {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(username, password);
|
||||||
|
onLoginSuccess();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Login failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-container">
|
||||||
|
<div className="auth-card">
|
||||||
|
<h1 className="auth-title">Welcome to Navix</h1>
|
||||||
|
<h2 className="auth-subtitle">Sign in to your account</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="auth-form">
|
||||||
|
{error && <div className="auth-error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="auth-input-group">
|
||||||
|
<label htmlFor="username">Username</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="Enter your username"
|
||||||
|
required
|
||||||
|
autoComplete="username"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-input-group">
|
||||||
|
<label htmlFor="password">Password</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
required
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="auth-button" disabled={loading}>
|
||||||
|
{loading ? 'Signing in...' : 'Sign In'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="auth-footer">
|
||||||
|
<p>
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={onSwitchToRegister}
|
||||||
|
className="auth-link"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Create one
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
138
frontend/src/components/Register.jsx
Normal file
138
frontend/src/components/Register.jsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { register } from '../services/auth';
|
||||||
|
import '../style/Auth.css';
|
||||||
|
|
||||||
|
function Register({ onRegisterSuccess, onSwitchToLogin }) {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (username.length < 3) {
|
||||||
|
setError('Username must be at least 3 characters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError('Password must be at least 6 characters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Attempting registration...', { username, email });
|
||||||
|
await register(username, email, password);
|
||||||
|
console.log('Registration successful');
|
||||||
|
onRegisterSuccess();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Registration error:', err);
|
||||||
|
setError(err.message || 'Registration failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-container">
|
||||||
|
<div className="auth-card">
|
||||||
|
<h1 className="auth-title">Join Navix</h1>
|
||||||
|
<h2 className="auth-subtitle">Create your account</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="auth-form">
|
||||||
|
{error && <div className="auth-error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="auth-input-group">
|
||||||
|
<label htmlFor="username">Username</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="Choose a username"
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
autoComplete="username"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-input-group">
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="Enter your email"
|
||||||
|
required
|
||||||
|
autoComplete="email"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-input-group">
|
||||||
|
<label htmlFor="password">Password</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Choose a password"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="auth-input-group">
|
||||||
|
<label htmlFor="confirmPassword">Confirm Password</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="Confirm your password"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="auth-button" disabled={loading}>
|
||||||
|
{loading ? 'Creating account...' : 'Create Account'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="auth-footer">
|
||||||
|
<p>
|
||||||
|
Already have an account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={onSwitchToLogin}
|
||||||
|
className="auth-link"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register;
|
||||||
@ -1,7 +1,20 @@
|
|||||||
const API_BASE = window?.ENV?.API_BASE || "";
|
const API_BASE = window?.ENV?.API_BASE || "http://localhost:8000/api";
|
||||||
|
|
||||||
|
// Get auth token from localStorage
|
||||||
|
function getAuthHeader() {
|
||||||
|
const token = localStorage.getItem('navix_token');
|
||||||
|
console.log('Token for request:', token ? `${token.substring(0, 20)}...` : 'NO TOKEN');
|
||||||
|
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchSections() {
|
export async function fetchSections() {
|
||||||
const res = await fetch(`${API_BASE}/apps`);
|
console.log('Fetching sections with auth...');
|
||||||
|
const res = await fetch(`${API_BASE}/sections`, {
|
||||||
|
headers: {
|
||||||
|
...getAuthHeader()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Sections response status:', res.status);
|
||||||
if (!res.ok) throw new Error('Failed to fetch sections');
|
if (!res.ok) throw new Error('Failed to fetch sections');
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|||||||
96
frontend/src/services/auth.js
Normal file
96
frontend/src/services/auth.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
const API_BASE = window?.ENV?.API_BASE || "http://localhost:8000/api";
|
||||||
|
|
||||||
|
const TOKEN_KEY = 'navix_token';
|
||||||
|
const USER_KEY = 'navix_user';
|
||||||
|
|
||||||
|
// Store token and user info
|
||||||
|
export function setAuthData(token, user) {
|
||||||
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
|
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored token
|
||||||
|
export function getToken() {
|
||||||
|
return localStorage.getItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stored user
|
||||||
|
export function getUser() {
|
||||||
|
const user = localStorage.getItem(USER_KEY);
|
||||||
|
return user ? JSON.parse(user) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
export function isAuthenticated() {
|
||||||
|
return !!getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear auth data (logout)
|
||||||
|
export function clearAuthData() {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
localStorage.removeItem(USER_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register new user
|
||||||
|
export async function register(username, email, password) {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.detail || 'Registration failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
console.log('Registration response:', data);
|
||||||
|
console.log('Token:', data.access_token);
|
||||||
|
setAuthData(data.access_token, data.user);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login user
|
||||||
|
export async function login(username, password) {
|
||||||
|
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.detail || 'Login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
setAuthData(data.access_token, data.user);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout user
|
||||||
|
export function logout() {
|
||||||
|
clearAuthData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current user from API
|
||||||
|
export async function getCurrentUser() {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) throw new Error('Not authenticated');
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/auth/me`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
clearAuthData();
|
||||||
|
}
|
||||||
|
throw new Error('Failed to get user info');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
169
frontend/src/style/Auth.css
Normal file
169
frontend/src/style/Auth.css
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
.auth-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #121212;
|
||||||
|
padding: 2rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
background: rgba(30, 30, 30, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'Orbitron', 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 2.5rem 0;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-error {
|
||||||
|
background: rgba(255, 59, 48, 0.15);
|
||||||
|
color: #ff6b6b;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border: 1px solid rgba(255, 59, 48, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-group label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-group input {
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-group input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-group input:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #121212;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button:hover:not(:disabled) {
|
||||||
|
background: #ffffff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
.auth-footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer p {
|
||||||
|
margin: 0;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link:hover:not(:disabled) {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}auth-link:hover:not(:disabled) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.auth-card {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
values.yaml
Normal file
47
values.yaml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
frontend:
|
||||||
|
image:
|
||||||
|
repository: harbor.dvirlabs.com/my-apps/navix-frontend
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
tag: master-e56328b
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: traefik
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
hosts:
|
||||||
|
- host: navix.dvirlabs.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
env:
|
||||||
|
API_BASE: "https://api-navix.dvirlabs.com/api"
|
||||||
|
MINIO_ENDPOINT: "s3.dvirlabs.com"
|
||||||
|
MINIO_BUCKET: "navix-icons"
|
||||||
|
backend:
|
||||||
|
image:
|
||||||
|
repository: harbor.dvirlabs.com/my-apps/navix-backend
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
tag: master-62a2769
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8000
|
||||||
|
env:
|
||||||
|
MINIO_ACCESS_KEY: "your-access-key"
|
||||||
|
MINIO_SECRET_KEY: "your-secret-key"
|
||||||
|
MINIO_ENDPOINT: "s3.dvirlabs.com"
|
||||||
|
MINIO_BUCKET: "navix-icons"
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: traefik
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
hosts:
|
||||||
|
- host: api-navix.dvirlabs.com
|
||||||
|
paths:
|
||||||
|
- path: /api
|
||||||
|
pathType: Prefix
|
||||||
Loading…
x
Reference in New Issue
Block a user