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.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel 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 from run_scripts import run_scripts, update_requirements, update_environment
import uvicorn import uvicorn
from passlib.context import CryptContext from passlib.context import CryptContext
@@ -49,6 +58,11 @@ class UserCreate(BaseModel):
password: str password: str
class UserResponse(BaseModel):
id: int
username: str
class Token(BaseModel): class Token(BaseModel):
access_token: str access_token: str
token_type: str token_type: str
@@ -87,6 +101,47 @@ def hello():
return {"message": "Welcome to the Project Monitor API"} 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) @app.post("/register", response_model=Token)
def register(user: UserCreate): def register(user: UserCreate):
db = SessionLocal() db = SessionLocal()

View File

@@ -1,10 +1,25 @@
import { env } from '$env/dynamic/public'; 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'; export const API_URL = env.PUBLIC_API_URL || 'http://localhost:8000';
// Helper to get token from localStorage // Helper to get token from localStorage
export function getToken(): string | null { 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 // 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 // Get subscription notifications with pagination
export async function fetchSubscriptionNotifications( export async function fetchSubscriptionNotifications(
subscriptionId: string, subscriptionId: string,

View File

@@ -30,7 +30,13 @@
} }
const data = await response.json(); 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()); goto('/').then(() => location.reload());
} catch (err) { } catch (err) {
loginError = 'Network error - ' + err; loginError = 'Network error - ' + err;

View File

@@ -122,6 +122,11 @@
<button class="p-2 w-full text-left" onclick={() => openNotificationPopup(notification)}> <button class="p-2 w-full text-left" onclick={() => openNotificationPopup(notification)}>
<div> <div>
<p class="font-semibold">{notification.title}</p> <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"> <p class="text-sm text-gray-500">
{new Date(notification.created_at).toLocaleString()} {new Date(notification.created_at).toLocaleString()}
</p> </p>

View File

@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fetchUserSettings, updateSetting } from '$lib/api'; import { fetchUserSettings, updateSetting, fetchUsers, updateUser, deleteUser } from '$lib/api';
import type { Settings } from '$lib/api'; import type { Settings, User } from '$lib/api';
import CodeMirror from 'svelte-codemirror-editor'; import CodeMirror from 'svelte-codemirror-editor';
let settings: Settings | null = $state(null); let settings: Settings | null = $state(null);
let users: User[] = $state([]);
let currentUser: string | null = $state(localStorage.getItem('username'));
let isLoading = $state(false); let isLoading = $state(false);
let error: string | null = $state(null); 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) { async function saveSetting(setting: Settings) {
isLoading = true; isLoading = true;
error = null; 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(() => { onMount(() => {
loadSettings(); loadSettings();
loadUsers();
}); });
</script> </script>
@@ -51,14 +92,14 @@
<div class="p-4 border rounded shadow"> <div class="p-4 border rounded shadow">
<label class="block mb-2 font-bold"> <label class="block mb-2 font-bold">
Requirements Requirements
<div class="w-full border rounded"> <div class="w-full border rounded font-normal">
<CodeMirror bind:value={settings.requirements} /> <CodeMirror bind:value={settings.requirements} />
</div> </div>
</label> </label>
<label class="block mt-4 mb-2 font-bold"> <label class="block mt-4 mb-2 font-bold">
Environment Environment
<div class="w-full border rounded"> <div class="w-full border rounded font-normal">
<CodeMirror bind:value={settings.environment} /> <CodeMirror bind:value={settings.environment} />
</div> </div>
</label> </label>
@@ -80,6 +121,65 @@
</button> </button>
</div> </div>
</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} {/if}
</div> </div>