Add dev/
This commit is contained in:
parent
deaa10181a
commit
4fc279e17c
@ -100,6 +100,9 @@ steps:
|
||||
|
||||
|
||||
trigger-gitops-via-push:
|
||||
when:
|
||||
branch: [ master, develop ]
|
||||
event: [ push ]
|
||||
name: Trigger apps-gitops via Git push
|
||||
image: alpine/git
|
||||
environment:
|
||||
|
||||
12
backend/.env
12
backend/.env
@ -1,4 +1,14 @@
|
||||
MINIO_ACCESS_KEY=TDJvsBmbkpUXpCw5M7LA
|
||||
MINIO_SECRET_KEY=n9scR7W0MZy6FF0bznV98fSgXpdebIQjqZvEr1Yu
|
||||
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
|
||||
url: https://minio.dvirlabs.com
|
||||
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.responses import JSONResponse
|
||||
import uvicorn
|
||||
@ -6,10 +6,32 @@ import os
|
||||
from dotenv import load_dotenv
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
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()
|
||||
|
||||
app.add_middleware(
|
||||
@ -36,35 +58,292 @@ minio_client = Minio(
|
||||
BUCKET = MINIO_BUCKET or "navix-icons"
|
||||
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("/")
|
||||
def root():
|
||||
return {"message": "Welcome to the FastAPI application!"}
|
||||
return {"message": "Welcome to Navix API v2.0!"}
|
||||
|
||||
|
||||
@router.get("/apps")
|
||||
def get_apps():
|
||||
"""Legacy endpoint - returns apps from YAML file"""
|
||||
if not APPS_FILE.exists():
|
||||
return {"error": "apps.yaml not found"}
|
||||
with open(APPS_FILE, "r") as 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")
|
||||
def add_app(entry: AppEntry):
|
||||
"""Legacy endpoint - adds app to YAML file"""
|
||||
if not APPS_FILE.exists():
|
||||
current = {"sections": []}
|
||||
else:
|
||||
@ -89,6 +368,7 @@ def add_app(entry: AppEntry):
|
||||
|
||||
@router.post("/edit_app")
|
||||
def edit_app(entry: AppEntry):
|
||||
"""Legacy endpoint - edits app in YAML file"""
|
||||
if not APPS_FILE.exists():
|
||||
return {"error": "apps.yaml not found"}
|
||||
|
||||
@ -116,6 +396,7 @@ def edit_app(entry: AppEntry):
|
||||
|
||||
@router.post("/delete_app")
|
||||
def delete_app(entry: AppEntry):
|
||||
"""Legacy endpoint - deletes app from YAML file"""
|
||||
if not APPS_FILE.exists():
|
||||
return {"error": "apps.yaml not found"}
|
||||
|
||||
@ -142,13 +423,18 @@ def delete_app(entry: AppEntry):
|
||||
|
||||
@router.get("/icon/{filename}")
|
||||
def get_public_icon_url(filename: str):
|
||||
"""Get public URL for an icon from MinIO"""
|
||||
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
|
||||
return JSONResponse(content={"url": url})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# App Registration
|
||||
# ============================================================================
|
||||
|
||||
app.include_router(router, prefix="/api")
|
||||
|
||||
|
||||
# ✅ This is the missing part:
|
||||
if __name__ == "__main__":
|
||||
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
|
||||
pydantic==2.8.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
|
||||
Pygments==2.18.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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
width: 100%;
|
||||
|
||||
@ -5,9 +5,11 @@ import AppModal from './components/AppModal';
|
||||
import ConfirmDialog from './components/ConfirmDialog';
|
||||
import Clock from './components/Clock';
|
||||
import Calendar from './components/Calendar';
|
||||
import Login from './components/Login';
|
||||
import Register from './components/Register';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
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 {
|
||||
fetchSections,
|
||||
@ -15,6 +17,7 @@ import {
|
||||
editAppInSection,
|
||||
deleteAppFromSection,
|
||||
} from './services/api';
|
||||
import { isAuthenticated, logout, getUser } from './services/auth';
|
||||
|
||||
function App() {
|
||||
const [sections, setSections] = useState([]);
|
||||
@ -22,19 +25,54 @@ function App() {
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [confirmData, setConfirmData] = useState(null);
|
||||
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 = () => {
|
||||
if (!authenticated) return;
|
||||
|
||||
fetchSections()
|
||||
.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);
|
||||
})
|
||||
.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(() => {
|
||||
loadSections();
|
||||
}, []);
|
||||
if (authenticated) {
|
||||
loadSections();
|
||||
}
|
||||
}, [authenticated]);
|
||||
|
||||
const handleDelete = (app) => {
|
||||
setConfirmData({
|
||||
@ -96,6 +134,14 @@ function App() {
|
||||
),
|
||||
})).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 (
|
||||
<div className="App">
|
||||
<div className="top-right-container">
|
||||
@ -103,6 +149,14 @@ function App() {
|
||||
<Clock />
|
||||
</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">
|
||||
<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() {
|
||||
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');
|
||||
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