Compare commits
4 Commits
dff07ef340
...
678eb22d2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678eb22d2b | ||
|
|
e913eb34f2 | ||
|
|
2f9f75ee3c | ||
|
|
45b78d5faf |
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user