Compare commits
9 Commits
8eef535e02
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678eb22d2b | ||
|
|
e913eb34f2 | ||
|
|
2f9f75ee3c | ||
|
|
45b78d5faf | ||
|
|
dff07ef340 | ||
|
|
625b231de5 | ||
|
|
013ddb26c7 | ||
|
|
657a224163 | ||
|
|
c957d839dd |
@@ -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()
|
||||||
@@ -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")
|
@app.get("/notifications")
|
||||||
def list_notifications(current_user: User = Depends(get_current_user)):
|
def list_notifications(current_user: User = Depends(get_current_user)):
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|||||||
@@ -3,3 +3,7 @@ uvicorn
|
|||||||
fastapi
|
fastapi
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
alembic
|
alembic
|
||||||
|
passlib
|
||||||
|
python-jose
|
||||||
|
argon2_cffi
|
||||||
|
python-multipart
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export default defineConfig(
|
|||||||
rules: {
|
rules: {
|
||||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||||
'no-undef': 'off'
|
'no-undef': 'off',
|
||||||
|
'svelte/no-navigation-without-resolve': 'off'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -245,6 +260,28 @@ export async function addSubscription(topic: string): Promise<Subscription> {
|
|||||||
return response.json();
|
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
|
// Delete a subscription
|
||||||
export async function deleteSubscription(subscriptionId: number): Promise<void> {
|
export async function deleteSubscription(subscriptionId: number): Promise<void> {
|
||||||
const response = await fetch(`${API_URL}/subscriptions/${subscriptionId}`, {
|
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
|
// Get subscription notifications with pagination
|
||||||
export async function fetchSubscriptionNotifications(
|
export async function fetchSubscriptionNotifications(
|
||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { API_URL } from '$lib/api';
|
import { API_URL } from '$lib/api';
|
||||||
|
|
||||||
@@ -31,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;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<script lang="ts">
|
<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';
|
import type { Notification, Subscription } from '$lib/api';
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -15,7 +21,7 @@
|
|||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
try {
|
try {
|
||||||
await Promise.all(notifications.map((notification) => deleteNotification(notification.id)));
|
deleteSubscriptionNotifications(subscription!.id);
|
||||||
notifications = [];
|
notifications = [];
|
||||||
window.showNotification('success', 'All notifications deleted successfully.');
|
window.showNotification('success', 'All notifications deleted successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -58,11 +64,7 @@
|
|||||||
}
|
}
|
||||||
async function markAllViewed() {
|
async function markAllViewed() {
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
markAllNotificationsAsViewed(subscription!.id);
|
||||||
notifications
|
|
||||||
.filter((notification) => !notification.viewed)
|
|
||||||
.map((notification) => setViewed(notification.id))
|
|
||||||
);
|
|
||||||
notifications = notifications.map((notification) =>
|
notifications = notifications.map((notification) =>
|
||||||
notification.viewed ? notification : { ...notification, viewed: true }
|
notification.viewed ? notification : { ...notification, viewed: true }
|
||||||
);
|
);
|
||||||
@@ -76,21 +78,21 @@
|
|||||||
async function loadMoreNotifications() {
|
async function loadMoreNotifications() {
|
||||||
loadingMore = true;
|
loadingMore = true;
|
||||||
try {
|
try {
|
||||||
const more = await fetchSubscriptionNotifications(subscription.id.toString(), limit, offset);
|
const more = await fetchSubscriptionNotifications(subscription!.id.toString(), limit, offset);
|
||||||
notifications = [...notifications, ...more];
|
notifications = [...notifications, ...more];
|
||||||
offset += more.length;
|
offset += more.length;
|
||||||
if (more.length < limit) {
|
if (more.length < limit) {
|
||||||
allLoaded = true;
|
allLoaded = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
window.showNotification('error', 'Failed to load more notifications');
|
window.showNotification('error', 'Failed to load more notifications - ' + err);
|
||||||
}
|
}
|
||||||
loadingMore = false;
|
loadingMore = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="p-4">
|
<main class="p-4">
|
||||||
<h1 class="text-2xl font-bold mb-4">Notifications for {subscription.topic}:</h1>
|
<h1 class="text-2xl font-bold mb-4">Notifications for {subscription!.topic}:</h1>
|
||||||
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a>
|
<a href="/notifications" class="text-blue-500 hover:underline">← Return to Subscriptions</a>
|
||||||
@@ -120,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>
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
localStorage.setItem('token', data.access_token);
|
localStorage.setItem('token', data.access_token);
|
||||||
goto('/');
|
goto('/').then(() => location.reload());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = 'Network error';
|
error = 'Network error - ' + err;
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let params: { id: string };
|
export let params: { id: string };
|
||||||
let script: Script = null;
|
let script: Script | null = null;
|
||||||
let logs: Log[] = [];
|
let logs: Log[] = [];
|
||||||
let updatedTitle: string = '';
|
let updatedTitle: string = '';
|
||||||
let updatedContent: string = '';
|
let updatedContent: string = '';
|
||||||
@@ -57,10 +57,10 @@
|
|||||||
|
|
||||||
async function handleExecuteScript() {
|
async function handleExecuteScript() {
|
||||||
try {
|
try {
|
||||||
await executeScript(script.id);
|
await executeScript(script!.id);
|
||||||
window.showNotification('success', 'Script executed successfully!');
|
window.showNotification('success', 'Script executed successfully!');
|
||||||
// Reload the list of logs after execution
|
// Reload the list of logs after execution
|
||||||
logs = (await fetchLogs(script.id)).sort(
|
logs = (await fetchLogs(script!.id)).sort(
|
||||||
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
|
(a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
async function handleDeleteLog(logId: number) {
|
async function handleDeleteLog(logId: number) {
|
||||||
try {
|
try {
|
||||||
await deleteLog(script.id, logId);
|
await deleteLog(script!.id, logId);
|
||||||
logs = logs.filter((log) => log.id !== logId);
|
logs = logs.filter((log) => log.id !== logId);
|
||||||
window.showNotification('success', 'Log deleted successfully!');
|
window.showNotification('success', 'Log deleted successfully!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -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 = $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>
|
||||||
@@ -74,12 +115,71 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
onclick={() => saveSetting(settings)}
|
onclick={() => saveSetting(settings!)}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</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