Compare commits

...

4 Commits

Author SHA1 Message Date
Sami Abuzakuk
678eb22d2b Fix small bug
All checks were successful
Build Container / build (push) Successful in 3m49s
2025-11-01 22:23:08 +01:00
Sami Abuzakuk
e913eb34f2 Add user frontend support 2025-11-01 22:21:06 +01:00
Sami Abuzakuk
2f9f75ee3c Add user api 2025-11-01 22:20:59 +01:00
Sami Abuzakuk
45b78d5faf Update token handling (added expiration time) 2025-11-01 21:58:20 +01:00
5 changed files with 226 additions and 7 deletions

View File

@@ -3,7 +3,16 @@ from fastapi import FastAPI, Depends, HTTPException, status, Query
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from model import Log, SessionLocal, Script, Settings, Subscription, Notification, User
from model import (
Base,
Log,
SessionLocal,
Script,
Settings,
Subscription,
Notification,
User,
)
from run_scripts import run_scripts, update_requirements, update_environment
import uvicorn
from passlib.context import CryptContext
@@ -49,6 +58,11 @@ class UserCreate(BaseModel):
password: str
class UserResponse(BaseModel):
id: int
username: str
class Token(BaseModel):
access_token: str
token_type: str
@@ -87,6 +101,47 @@ def hello():
return {"message": "Welcome to the Project Monitor API"}
# User Management Endpoints
@app.get("/users", response_model=list[UserResponse])
def list_users(current_user: User = Depends(get_current_user)):
db = SessionLocal()
users = db.query(User).all()
db.close()
return users
@app.put("/users/{user_id}")
def update_user(
user_id: int,
user: UserCreate,
current_user: User = Depends(get_current_user),
):
db = SessionLocal()
existing_user = db.query(User).filter(User.id == user_id).first()
if not existing_user:
db.close()
raise HTTPException(status_code=404, detail="User not found")
existing_user.username = user.username
existing_user.password_hash = get_password_hash(user.password)
db.commit()
db.refresh(existing_user)
db.close()
return {"message": "User updated successfully"}
@app.delete("/users/{user_id}")
def delete_user(user_id: int, current_user: User = Depends(get_current_user)):
db = SessionLocal()
user = db.query(User).filter(User.id == user_id).first()
if not user:
db.close()
raise HTTPException(status_code=404, detail="User not found")
db.delete(user)
db.commit()
db.close()
return {"message": "User deleted successfully"}
@app.post("/register", response_model=Token)
def register(user: UserCreate):
db = SessionLocal()

View File

@@ -1,10 +1,25 @@
import { env } from '$env/dynamic/public';
export interface User {
id: number;
username: string;
password: string | null;
}
export const API_URL = env.PUBLIC_API_URL || 'http://localhost:8000';
// Helper to get token from localStorage
export function getToken(): string | null {
return localStorage.getItem('token');
const tokenData = localStorage.getItem('token');
if (tokenData) {
const { value, expiresAt } = JSON.parse(tokenData);
if (Date.now() > expiresAt) {
localStorage.removeItem('token');
location.reload();
return null;
}
return value;
}
return null;
}
// Helper to add Authorization header if token exists
@@ -278,6 +293,44 @@ export async function deleteSubscription(subscriptionId: number): Promise<void>
}
}
// Fetch all users
export async function fetchUsers(): Promise<User[]> {
const response = await fetch(`${API_URL}/users`, {
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
}
// Update a user
export async function updateUser(
userId: number,
username: string,
password: string
): Promise<void> {
const response = await fetch(`${API_URL}/users/${userId}`, {
method: 'PUT',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error('Failed to update user');
}
}
// Delete a user
export async function deleteUser(userId: number): Promise<void> {
const response = await fetch(`${API_URL}/users/${userId}`, {
method: 'DELETE',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
}
// Get subscription notifications with pagination
export async function fetchSubscriptionNotifications(
subscriptionId: string,

View File

@@ -30,7 +30,13 @@
}
const data = await response.json();
localStorage.setItem('token', data.access_token);
const expirationTime = Date.now() + 60 * 60 * 1000; // 60 minutes in milliseconds
localStorage.setItem(
'token',
JSON.stringify({ value: data.access_token, expiresAt: expirationTime })
);
localStorage.setItem('username', loginUsername);
goto('/').then(() => location.reload());
} catch (err) {
loginError = 'Network error - ' + err;

View File

@@ -122,6 +122,11 @@
<button class="p-2 w-full text-left" onclick={() => openNotificationPopup(notification)}>
<div>
<p class="font-semibold">{notification.title}</p>
<p>
{notification.message.split('\n')[0].length > 75
? `${notification.message.split('\n')[0].slice(0, 75)}...`
: notification.message.split('\n')[0]}
</p>
<p class="text-sm text-gray-500">
{new Date(notification.created_at).toLocaleString()}
</p>

View File

@@ -1,10 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fetchUserSettings, updateSetting } from '$lib/api';
import type { Settings } from '$lib/api';
import { fetchUserSettings, updateSetting, fetchUsers, updateUser, deleteUser } from '$lib/api';
import type { Settings, User } from '$lib/api';
import CodeMirror from 'svelte-codemirror-editor';
let settings: Settings | null = $state(null);
let users: User[] = $state([]);
let currentUser: string | null = $state(localStorage.getItem('username'));
let isLoading = $state(false);
let error: string | null = $state(null);
@@ -21,6 +23,18 @@
}
}
async function loadUsers() {
isLoading = true;
error = null;
try {
users = await fetchUsers();
} catch (err) {
error = 'Failed to load users - ' + err;
} finally {
isLoading = false;
}
}
async function saveSetting(setting: Settings) {
isLoading = true;
error = null;
@@ -34,8 +48,35 @@
}
}
async function handleUpdateUser(user: User, username: string, password: string) {
isLoading = true;
error = null;
try {
await updateUser(user.id, username, password);
loadUsers(); // Refresh users after update
} catch (err) {
error = 'Failed to update user - ' + err;
} finally {
isLoading = false;
}
}
async function handleDeleteUser(userId: number) {
isLoading = true;
error = null;
try {
await deleteUser(userId);
loadUsers(); // Refresh users after deletion
} catch (err) {
error = 'Failed to delete user - ' + err;
} finally {
isLoading = false;
}
}
onMount(() => {
loadSettings();
loadUsers();
});
</script>
@@ -51,14 +92,14 @@
<div class="p-4 border rounded shadow">
<label class="block mb-2 font-bold">
Requirements
<div class="w-full border rounded">
<div class="w-full border rounded font-normal">
<CodeMirror bind:value={settings.requirements} />
</div>
</label>
<label class="block mt-4 mb-2 font-bold">
Environment
<div class="w-full border rounded">
<div class="w-full border rounded font-normal">
<CodeMirror bind:value={settings.environment} />
</div>
</label>
@@ -80,6 +121,65 @@
</button>
</div>
</div>
<h2 class="text-xl font-bold mt-8">User Management</h2>
{#if isLoading}
<p>Loading users...</p>
{:else if error}
<p class="text-red-500">{$error}</p>
{:else}
<table
class="table-auto w-full mt-4 border-collapse border border-gray-300 shadow-lg rounded-lg"
>
<thead>
<tr class="bg-gray-100">
<th class="px-4 py-2 border border-gray-300 text-left font-semibold">Users</th>
<th class="px-4 py-2 border border-gray-300 text-center font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{#each users as user (user.id)}
<tr class="hover:bg-gray-50">
<td class="border px-4 py-2 border-gray-300">
{#if user.username === currentUser}
<input
type="text"
placeholder="New Username"
class="px-2 py-1 border rounded"
bind:value={user.username}
/>
<input
type="password"
placeholder="New Password"
class="px-2 py-1 border rounded"
bind:value={user.password}
/>
{:else}
{user.username}
{/if}
</td>
<td class="border px-4 py-2 border-gray-300 text-center">
{#if user.username === currentUser}
<button
class="ml-2 px-2 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600"
onclick={() => handleUpdateUser(user, user.username, user.password)}
>
Update
</button>
{:else}
<button
class="ml-2 px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600"
onclick={() => handleDeleteUser(user.id)}
>
Delete
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
{/if}
</div>