Compare commits

...

7 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
Sami Abuzakuk
dff07ef340 Frontend support for mark all viewed and delete all
All checks were successful
Build Container / build (push) Successful in 3m48s
2025-11-01 17:05:24 +01:00
Sami Abuzakuk
625b231de5 Add backend for set all viewed and delete all 2025-11-01 17:05:10 +01:00
Sami Abuzakuk
013ddb26c7 Fix no reload on register 2025-11-01 16:41:52 +01:00
6 changed files with 293 additions and 15 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()
@@ -242,6 +297,41 @@ def list_subscription_notifications(
]
@app.post("/subscriptions/{subscription_id}/notifications")
def set_all_notifications_viewed(
subscription_id: int,
current_user: User = Depends(get_current_user),
):
db = SessionLocal()
notifications = (
db.query(Notification)
.filter(Notification.subscription_id == subscription_id)
.all()
)
for notification in notifications:
notification.viewed = True
db.commit()
db.close()
return {"message": "Notifications marked as viewed"}
@app.delete("/subscriptions/{subscription_id}/notifications")
def remove_subscription_notifications(
subscription_id: int, current_user: User = Depends(get_current_user)
):
db = SessionLocal()
notifications = (
db.query(Notification)
.filter(Notification.subscription_id == subscription_id)
.all()
)
for notification in notifications:
db.delete(notification)
db.commit()
db.close()
return {"message": "Notifications removed"}
@app.get("/notifications")
def list_notifications(current_user: User = Depends(get_current_user)):
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
@@ -245,6 +260,28 @@ export async function addSubscription(topic: string): Promise<Subscription> {
return response.json();
}
// Add a new notification to a specific subscription
export async function markAllNotificationsAsViewed(subscriptionId: number): Promise<void> {
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}/notifications`, {
method: 'POST',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to mark all notifications as viewed for subscription');
}
}
// Delete all notifications for a specific subscription
export async function deleteSubscriptionNotifications(subscriptionId: number): Promise<void> {
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}/notifications`, {
method: 'DELETE',
headers: authHeaders()
});
if (!response.ok) {
throw new Error('Failed to delete notifications for subscription');
}
}
// Delete a subscription
export async function deleteSubscription(subscriptionId: number): Promise<void> {
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}`, {
@@ -256,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

@@ -1,5 +1,11 @@
<script lang="ts">
import { deleteNotification, setViewed, fetchSubscriptionNotifications } from '$lib/api';
import {
deleteNotification,
setViewed,
fetchSubscriptionNotifications,
deleteSubscriptionNotifications,
markAllNotificationsAsViewed
} from '$lib/api';
import type { Notification, Subscription } from '$lib/api';
let { data } = $props();
@@ -15,7 +21,7 @@
);
if (!confirmed) return;
try {
await Promise.all(notifications.map((notification) => deleteNotification(notification.id)));
deleteSubscriptionNotifications(subscription!.id);
notifications = [];
window.showNotification('success', 'All notifications deleted successfully.');
} catch (error) {
@@ -58,11 +64,7 @@
}
async function markAllViewed() {
try {
await Promise.all(
notifications
.filter((notification) => !notification.viewed)
.map((notification) => setViewed(notification.id))
);
markAllNotificationsAsViewed(subscription!.id);
notifications = notifications.map((notification) =>
notification.viewed ? notification : { ...notification, viewed: true }
);
@@ -120,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

@@ -27,7 +27,7 @@
}
const data = await response.json();
localStorage.setItem('token', data.access_token);
goto('/');
goto('/').then(() => location.reload());
} catch (err) {
error = 'Network error - ' + err;
} finally {

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>