Compare commits

..

No commits in common. "4fc279e17cad8c885457626bb7431bb9fd1ba982" and "f87719e03b5d76c6ff24b191c82c660ebbe1ce9f" have entirely different histories.

27 changed files with 30 additions and 1683 deletions

View File

@ -100,9 +100,6 @@ 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:

View File

@ -2,13 +2,3 @@ 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

View File

@ -1,15 +0,0 @@
# 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

View File

@ -1,135 +0,0 @@
# 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.

View File

@ -45,15 +45,3 @@ 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

View File

@ -1,81 +0,0 @@
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"},
)

View File

@ -1,56 +0,0 @@
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

View File

@ -1,4 +1,4 @@
from fastapi import FastAPI, APIRouter, Depends, HTTPException, status from fastapi import FastAPI, APIRouter
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,32 +6,10 @@ 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
from database import get_db_cursor, init_db app = FastAPI()
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(
@ -58,292 +36,35 @@ 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 Navix API v2.0!"} return {"message": "Welcome to the FastAPI application!"}
@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:
@ -368,7 +89,6 @@ 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"}
@ -396,7 +116,6 @@ 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"}
@ -423,18 +142,13 @@ 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)

View File

@ -1,77 +0,0 @@
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

View File

@ -36,9 +36,6 @@ 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

View File

@ -1,75 +0,0 @@
-- 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;

View File

@ -12,60 +12,10 @@ body {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 2rem; padding: 2rem;
padding-top: 9rem;
min-height: 100vh; min-height: 100vh;
text-align: center; text-align: center;
} }
.top-right-container {
position: absolute;
top: 1.5rem;
right: 2rem;
display: flex;
gap: 1rem;
align-items: flex-start;
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%;

View File

@ -4,12 +4,9 @@ import SectionGrid from './components/SectionGrid';
import AppModal from './components/AppModal'; 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 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, IoMdLogOut } from 'react-icons/io'; import { IoIosAdd } from 'react-icons/io';
import CustomToast from './components/CustomToast'; import CustomToast from './components/CustomToast';
import { import {
fetchSections, fetchSections,
@ -17,7 +14,6 @@ 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([]);
@ -25,54 +21,19 @@ 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 => {
// Handle both old format {sections: []} and new format [] const filtered = (data.sections || []).filter(section => (section.apps?.length ?? 0) > 0);
const sectionsArray = Array.isArray(data) ? data : (data.sections || []);
const filtered = sectionsArray.filter(section => (section.apps?.length ?? 0) > 0);
setSections(filtered); setSections(filtered);
}) })
.catch(err => { .catch(err => console.error('Failed to fetch sections:', 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({
@ -134,28 +95,9 @@ 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"> <Clock />
<Calendar />
<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"> <div className="title-wrapper">

View File

@ -1,92 +0,0 @@
import { useState, useEffect } from 'react';
import { FaChevronDown } from 'react-icons/fa';
import '../style/Calendar.css';
function Calendar() {
const [now, setNow] = useState(new Date());
const [collapsed, setCollapsed] = useState(false);
const [currentMonth, setCurrentMonth] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
const getDaysInMonth = (date) => {
const year = date.getFullYear();
const month = date.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
return { daysInMonth, startingDayOfWeek, year, month };
};
const { daysInMonth, startingDayOfWeek, year, month } = getDaysInMonth(currentMonth);
const days = [];
for (let i = 0; i < startingDayOfWeek; i++) {
days.push(null);
}
for (let i = 1; i <= daysInMonth; i++) {
days.push(i);
}
const isToday = (day) => {
return day === now.getDate() &&
month === now.getMonth() &&
year === now.getFullYear();
};
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const prevMonth = () => {
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1));
};
const nextMonth = () => {
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1));
};
return (
<div className={`calendar-container ${collapsed ? 'collapsed' : ''}`}>
<div className="calendar-header" onClick={() => setCollapsed(!collapsed)}>
<span className={`calendar-date ${collapsed ? 'collapsed' : ''}`}>{now.toLocaleDateString('en-GB', {
weekday: 'short',
day: 'numeric',
month: 'short',
year: 'numeric'
})}</span>
<FaChevronDown className={`calendar-toggle-icon ${collapsed ? 'collapsed' : ''}`} />
</div>
<div className={`calendar-body ${collapsed ? 'collapsed' : ''}`}>
<div className="calendar-month-nav">
<button onClick={prevMonth} className="calendar-nav-btn"></button>
<span className="calendar-month-year">{monthNames[month]} {year}</span>
<button onClick={nextMonth} className="calendar-nav-btn"></button>
</div>
<div className="calendar-grid">
{dayNames.map(day => (
<div key={day} className="calendar-day-name">{day}</div>
))}
{days.map((day, idx) => (
<div
key={idx}
className={`calendar-day ${day === null ? 'empty' : ''} ${isToday(day) ? 'today' : ''}`}
>
{day}
</div>
))}
</div>
</div>
</div>
);
}
export default Calendar;

View File

@ -1,85 +0,0 @@
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;

View File

@ -1,138 +0,0 @@
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;

View File

@ -1,20 +1,7 @@
const API_BASE = window?.ENV?.API_BASE || "http://localhost:8000/api"; const API_BASE = window?.ENV?.API_BASE || "";
// 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() {
console.log('Fetching sections with auth...'); const res = await fetch(`${API_BASE}/apps`);
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();
} }

View File

@ -1,96 +0,0 @@
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();
}

View File

@ -1,169 +0,0 @@
.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;
}
}

View File

@ -1,153 +0,0 @@
.calendar-container {
font-family: 'Rajdhani', sans-serif;
color: #00f0ff;
text-shadow: 0 0 6px rgba(0, 255, 255, 0.3);
user-select: none;
background: rgba(0, 20, 40, 0.8);
border: 1px solid rgba(0, 240, 255, 0.3);
border-radius: 10px;
padding: 0.7rem;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s ease;
width: auto;
position: absolute;
right: 200px;
}
.calendar-container.collapsed {
width: auto;
padding: 0.5rem;
background: transparent;
border: none;
backdrop-filter: none;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding-bottom: 0.4rem;
min-width: 30px;
}
.calendar-date {
font-size: 0.85rem;
color: #999;
white-space: nowrap;
overflow: hidden;
transition: all 0.3s ease;
}
.calendar-date.collapsed {
width: 0;
opacity: 0;
}
.calendar-toggle-icon {
font-size: 1.4rem;
color: #00f0ff;
transition: transform 0.3s ease;
flex-shrink: 0;
transform: rotate(-90deg);
}
.calendar-toggle-icon.collapsed {
transform: rotate(90deg);
}
.calendar-body {
width: 220px;
overflow: hidden;
transition: all 0.3s ease;
opacity: 1;
}
.calendar-body.collapsed {
width: 0;
opacity: 0;
}
.calendar-month-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.5rem 0;
padding-top: 0.4rem;
border-top: 1px solid rgba(0, 240, 255, 0.2);
}
.calendar-month-year {
font-size: 0.9rem;
font-weight: 500;
color: #00f0ff;
}
.calendar-nav-btn {
background: transparent;
border: 1px solid rgba(0, 240, 255, 0.3);
color: #00f0ff;
cursor: pointer;
font-size: 1.2rem;
width: 24px;
height: 24px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
padding: 0;
line-height: 1;
}
.calendar-nav-btn:hover {
background: rgba(0, 240, 255, 0.1);
border-color: #00f0ff;
transform: scale(1.1);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-top: 0.5rem;
}
.calendar-day-name {
text-align: center;
font-size: 0.65rem;
color: #00f0ff;
font-weight: 500;
padding: 0.2rem 0;
opacity: 0.7;
}
.calendar-day {
text-align: center;
padding: 0.35rem;
font-size: 0.75rem;
color: #999;
border-radius: 4px;
transition: all 0.2s ease;
}
.calendar-day:not(.empty):hover {
background: rgba(0, 240, 255, 0.1);
color: #00f0ff;
cursor: pointer;
}
.calendar-day.today {
background: rgba(0, 240, 255, 0.2);
color: #00f0ff;
font-weight: bold;
border: 1px solid #00f0ff;
box-shadow: 0 0 8px rgba(0, 240, 255, 0.4);
}
.calendar-day.empty {
visibility: hidden;
}

View File

@ -1,4 +1,7 @@
.clock-container { .clock-container {
position: absolute;
top: 1.5rem;
right: 2rem;
text-align: right; text-align: right;
font-family: 'Rajdhani', sans-serif; font-family: 'Rajdhani', sans-serif;
color: #00f0ff; color: #00f0ff;
@ -6,8 +9,6 @@
font-size: 1rem; font-size: 1rem;
line-height: 1.2; line-height: 1.2;
user-select: none; user-select: none;
position: absolute;
right: 0;
} }
.clock-time { .clock-time {

View File

@ -1,47 +0,0 @@
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