Compare commits

...
34 Commits
Author SHA1 Message Date
matt 1b9a2dd109 remove break before name 2026-10-28 22:31:54 -07:00
matt fc96702600 add break after name 2026-10-28 22:31:02 -07:00
matt efbfbe8441 move name out of div 2026-10-28 22:29:55 -07:00
matt 9163437130 change br to p 2026-10-28 22:28:57 -07:00
matt dd4b382d8c fix isset typo 2026-10-28 22:25:02 -07:00
matt 9dc05a72b5 add username to list_providers 2026-10-28 22:24:12 -07:00
matt 9e4c6571cf testing list providers remove leading slash 2026-10-28 22:19:00 -07:00
matt 2efd469f1e testing list providers 2026-10-28 22:15:39 -07:00
matt 5ec16d4cbc OIDC test - test jumbojett oidc round two try autologin 2026-10-28 22:09:48 -07:00
matt 49d2e818d0 OIDC test - test jumbojett oidc round two remove session start from index 2026-10-28 22:07:10 -07:00
matt ff8cf722f4 OIDC test - test jumbojett oidc round two move session start in auth to top 2026-10-28 22:06:00 -07:00
matt ca1e36b0b6 OIDC test - test jumbojett oidc round two add logoin 2026-10-28 22:02:08 -07:00
matt c5c1081589 OIDC test - test jumbojett oidc round two immediate login 2026-10-28 22:00:25 -07:00
matt 0dd3e43893 dockerfile - update composer path 2026-10-28 21:58:27 -07:00
matt 07d3895eb5 OIDC test - test jumbojett oidc round two change root paths 2026-10-28 21:56:05 -07:00
matt 4b7abac7ca OIDC test - test jumbojett oidc round two change calback path 2026-10-28 21:53:06 -07:00
matt e9bc19d5e2 OIDC test - test jumbojett oidc round two change vendor path 2026-10-28 21:48:09 -07:00
matt 124c5a1ff6 OIDC test - test jumbojett oidc round two add composer packages 2026-10-28 21:45:55 -07:00
matt 0cf83eb9d6 dockerfile - move api up in order 2026-10-28 21:44:34 -07:00
matt addf70b7bb OIDC test - test jumbojett oidc round two fix config path 2 2026-10-28 21:43:43 -07:00
matt 221d435480 OIDC test - test jumbojett oidc round two fix config path 2026-10-28 21:42:35 -07:00
matt 65a69b585d OIDC test - test jumbojett oidc round two 1 2026-10-28 21:40:23 -07:00
matt 6f8dd7bbe3 OIDC test - test jumbojett oidc php app test 6 2026-10-23 04:53:37 -07:00
matt 933134e3b0 OIDC test - test jumbojett oidc php app test add exception message 2026-10-23 04:51:05 -07:00
matt 58de6d1073 OIDC test - test jumbojett oidc php app test 4 2026-10-23 04:47:31 -07:00
matt ceb0ce1778 OIDC test - test jumbojett oidc php app test 3 2026-10-23 04:46:08 -07:00
matt 9ddcc36893 OIDC test - test jumbojett oidc php app test 2 2026-10-23 04:42:08 -07:00
matt 97a31db020 OIDC test - test jumbojett oidc php app 2026-10-23 04:38:37 -07:00
matt a759eb206f OIDC test - add package to composer 2026-10-23 03:51:16 -07:00
matt 5cd6084c3c OIDC test - change firebase version yet again 2026-10-23 03:45:47 -07:00
matt 59e99b74a7 OIDC test - change firebase version again 2026-10-23 03:44:21 -07:00
matt 33644b69c3 OIDC test - change firebase version 2026-10-23 03:36:01 -07:00
matt f2810a7950 OIDC test - move to www base 2026-10-23 03:25:45 -07:00
matt d572067cf6 OIDC test - move to www base 2026-10-23 03:25:41 -07:00
26 changed files with 175 additions and 383 deletions

No files matched your search

+17 -5
View File
@@ -10,14 +10,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
supervisor \ supervisor \
# Others # Others
net-tools curl less nano \ net-tools curl less nano \
# PHP extensions needed by the OIDC helpers
libzip-dev libpng-dev libicu-dev libpq-dev \
&& docker-php-ext-install pdo_mysql \
&& docker-php-ext-install zip \
&& docker-php-ext-install intl \
# Clean up # Clean up
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Apache on 8069
COPY apache_ports.conf /etc/apache2/ports.conf
COPY apache_vhost.conf /etc/apache2/sites-available/000-default.conf
COPY web/ /var/www/html/
# API on 5000 # API on 5000
RUN mkdir -p /opt/Fitness RUN mkdir -p /opt/Fitness
RUN python3 -m venv /opt/Fitness/venv RUN python3 -m venv /opt/Fitness/venv
@@ -26,6 +26,18 @@ RUN /opt/Fitness/venv/bin/pip3 install --no-cache-dir -r /opt/Fitness/requiremen
# Copy the actual app code after installing deps # Copy the actual app code after installing deps
COPY api/ /opt/Fitness/ COPY api/ /opt/Fitness/
# Install Composer (stand‑alone)
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Apache on 8069
COPY apache_ports.conf /etc/apache2/ports.conf
COPY apache_vhost.conf /etc/apache2/sites-available/000-default.conf
COPY web/ /var/www/
# Set up composer
WORKDIR /var/www/
RUN composer require jumbojett/openid-connect-php
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Add supervisord configuration # Add supervisord configuration
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
+4
View File
@@ -3,6 +3,10 @@ This app is intended to use with tracking personal fitness providers and visits
I will build this with LAPP stack - Linux, Apache, PHP & Python. I will build the front-end in PHP, and the back-end will be a Python API with a database. I will build this with LAPP stack - Linux, Apache, PHP & Python. I will build the front-end in PHP, and the back-end will be a Python API with a database.
The database will be a SQLite file handled by the Python service. There will need to be a Users table, a Providers table, and a Visits table. The Users table will contain the information and settings for the site users. The settings can be a JSON structure inside a single text entry. The Providers table will track the Providers info and rates. The rates will also be a JSON structure in a single text field. Any user can add a provider, and set it private or not. If not private, other users can add visits for the same provider. The Visits table will contain enties for User Visits to Providers. This will have date, time, location, provider, cost. There should be a initialize_db function that checks for the file and creates it if needed, including all tables. Here is a complete list of data for each table. The database will be a SQLite file handled by the Python service. There will need to be a Users table, a Providers table, and a Visits table. The Users table will contain the information and settings for the site users. The settings can be a JSON structure inside a single text entry. The Providers table will track the Providers info and rates. The rates will also be a JSON structure in a single text field. Any user can add a provider, and set it private or not. If not private, other users can add visits for the same provider. The Visits table will contain enties for User Visits to Providers. This will have date, time, location, provider, cost. There should be a initialize_db function that checks for the file and creates it if needed, including all tables. Here is a complete list of data for each table.
Found this for OIDC - don't wanna lose it
https://github.com/jumbojett/OpenID-Connect-PHP/blob/master/README.md
``` ```
Users Users
- Name - Name
+8
View File
@@ -0,0 +1,8 @@
{
"require": {
"jumbojett/openid-connect-php": "^1.0",
"vlucas/phpdotenv": "^5.4",
"guzzlehttp/guzzle": "^7.0",
"firebase/php-jwt": "^7.0"
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
// config.php
return [
// Base URL for the Authelia OpenID Provider
'provider_url' => 'https://auth.matt-cloud.com', // <-- change if you run locally
// Client (your application) credentials
'client_id' => 'fitness-oidc',
'client_secret' => 'egebuqzZfVnjjBG9JDQ8tjW3TzV9gJurvT7qfB6c2SX2nPBNeaGycDfx4788QzJd',
// The callback URL **must** match exactly what is in Authelia
'redirect_uri' => 'https://fitness.matt-cloud.com/callback.php',
// Scopes we want – Authelia already provides “openid”, “profile”, “email”, “groups”
'scopes' => ['openid', 'profile', 'email', 'groups'],
// Response type & grant type
'response_type' => 'code',
];
File renamed without changes.
@@ -5,7 +5,7 @@
// When the form is submitted, it POSTs JSON to /providers. // When the form is submitted, it POSTs JSON to /providers.
// file settings // file settings
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/../fitness-config.php';
// Helper to call the API ------------------------------------------- // Helper to call the API -------------------------------------------
function callApi(string $method, string $path, $body = null) function callApi(string $method, string $path, $body = null)
+28
View File
@@ -0,0 +1,28 @@
<?php
session_start();
require __DIR__ . '/../vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
$config = require __DIR__ . '/../config.php';
$oidc = new OpenIDConnectClient(
$config['provider_url'],
$config['client_id'],
$config['client_secret']
);
// Tell the library where to return after the user logs in
$oidc->setRedirectURL($config['redirect_uri']);
// Tell it which scopes we want
$oidc->addScope($config['scopes']);
// The OIDC flow we’ll use
$oidc->setResponseTypes([$config['response_type']]);
// Optional: for local development you can relax TLS validation
// $oidc->setVerifyHost(false);
// $oidc->setVerifyPeer(false);
$oidc->authenticate(); // This automatically redirects to Authelia
+33
View File
@@ -0,0 +1,33 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
session_start();
$config = require __DIR__ . '/../config.php';
$oidc = new OpenIDConnectClient(
$config['provider_url'],
$config['client_id'],
$config['client_secret']
);
$oidc->setRedirectURL($config['redirect_uri']);
$oidc->addScope($config['scopes']);
$oidc->setResponseTypes([$config['response_type']]);
// This will read the `code` query parameter, fetch the tokens
// and validate everything.
$oidc->authenticate();
// Fetch the OpenID Connect userinfo claims
$claims = $oidc->requestUserInfo(); // → stdClass with the claims
// Store what we need in the PHP session
$_SESSION['authenticated'] = true;
$_SESSION['name'] = $claims->name ?? $claims->given_name ?? $claims->family_name ?? '';
$_SESSION['email'] = $claims->email ?? '';
$_SESSION['groups'] = $claims->groups ?? [];
// Redirect back to the home page
header('Location: ../');
exit;
@@ -3,7 +3,7 @@
// Edit an existing fitness provider. // Edit an existing fitness provider.
// Handles rate options similar to add_provider.php. // Handles rate options similar to add_provider.php.
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/../fitness-config.php';
$provider_id = (int)($_GET['id'] ?? 0); $provider_id = (int)($_GET['id'] ?? 0);
if ($provider_id <= 0) { if ($provider_id <= 0) {
+50
View File
@@ -0,0 +1,50 @@
<?php
session_start();
$config = require __DIR__ . '/../config.php';
$debug_output = false;
if (!isset($_SESSION['authenticated'])) {
// User not logged in – show a login button
// echo '<h1>Welcome to the demo</h1>';
// echo '<p><a href="auth.php">Login with Authelia SSO</a></p>';
header('Location: auth.php');
exit;
}
// The user is logged in – display the attributes
$name = $_SESSION['name'] ?? 'unknown';
$email = $_SESSION['email'] ?? 'unknown';
$groups = $_SESSION['groups'] ?? [];
if ($debug_output) {
echo '<h1>Logged in as ' . htmlspecialchars($name) . '</h1>';
echo '<ul>';
echo '<li><strong>Email:</strong> ' . htmlspecialchars($email) . '</li>';
echo '<li><strong>Groups:</strong> ' . htmlspecialchars(implode(', ', $groups)) . '</li>';
echo '</ul>';
}
// echo '<p><a href="logout.php">Logout</a></p>';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fitness Tracker</title>
<link rel="stylesheet" href="https://cosmostat.matt-cloud.com/src/styles.css">
<style>
body { font-family: Arial, sans-serif; margin: 2rem; }
#search { margin-bottom: 1rem; padding: 0.5rem; width: 300px; }
table { width: 100%; border-collapse: collapse; }
img { max-width: 60px; height: auto; }
.header-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
</style>
</head>
<body>
<?php
include_once('list_providers.php');
?>
</body>
</html>
@@ -5,7 +5,7 @@
// The API endpoint for providers is /providers. // The API endpoint for providers is /providers.
// file settings // file settings
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/../fitness-config.php';
// --- Helper to call the API ------------------------------------------- // --- Helper to call the API -------------------------------------------
function callApi(string $method, string $path, $body = null) function callApi(string $method, string $path, $body = null)
{ {
@@ -77,6 +77,12 @@ unset($p);
<h1>Providers</h1> <h1>Providers</h1>
<a href="add_provider.php" class="btn">Add Provider</a> <a href="add_provider.php" class="btn">Add Provider</a>
</div> </div>
<?php
if (isset($_SESSION['authenticated'])){
echo "Welcome " . htmlspecialchars($_SESSION['name']);
echo "<p>";
}
?>
<input type="text" id="search" placeholder="Search providers..."> <input type="text" id="search" placeholder="Search providers...">
<table id="providers-table"> <table id="providers-table">
<thead> <thead>
+5
View File
@@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: ../');
exit;
File renamed without changes.
File renamed without changes.
@@ -5,7 +5,7 @@
// The design follows the pattern used in list_providers.php. // The design follows the pattern used in list_providers.php.
// file settings // file settings
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/../fitness-config.php';
// --- Helper to call the Flask API ------------------------------------------- // --- Helper to call the Flask API -------------------------------------------
function callApi(string $method, string $path, $body = null) function callApi(string $method, string $path, $body = null)
View File
Whitespace-only changes.
-9
View File
@@ -1,9 +0,0 @@
OIDC_CLIENT_ID=fitness-oidc
OIDC_CLIENT_SECRET=egebuqzZfVnjjBG9JDQ8tjW3TzV9gJurvT7qfB6c2SX2nPBNeaGycDfx4788QzJd
OIDC_REDIRECT_URI=https://fitness.matt-cloud.com/pgapi/auth/oidc/callback
OIDC_AUTH_ENDPOINT=https://auth.matt-cloud.com/oidc/auth
OIDC_TOKEN_ENDPOINT=https://auth.matt-cloud.com/oidc/token
OIDC_USERINFO_ENDPOINT=https://auth.matt-cloud.com/oidc/userinfo
OIDC_LOGOUT_ENDPOINT=https://auth.matt-cloud.com/logout
# Optional: the default redirect after logout
OIDC_POST_LOGOUT_REDIRECT_URI=https://fitness.matt-cloud.com/
-59
View File
@@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/vendor/autoload.php';
use Fitness\OIDC\OIDC;
// Ensure a state was sent back.
if (!isset($_GET['state']) || !isset($_SESSION['oidc_state'])) {
http_response_code(400);
exit('Missing state parameter.');
}
// Verify the state matches what we stored.
if ($_GET['state'] !== $_SESSION['oidc_state']) {
http_response_code(400);
exit('Invalid state.');
}
// Clean up the session variables.
$state = $_SESSION['oidc_state'];
$nonce = $_SESSION['oidc_nonce'];
unset($_SESSION['oidc_state'], $_SESSION['oidc_nonce']);
if (!isset($_GET['code'])) {
http_response_code(400);
exit('Missing authorization code.');
}
$code = $_GET['code'];
$oidc = new OIDC();
try {
// Exchange code for tokens.
$tokens = $oidc->exchangeCode($code);
// Validate ID token (minimal).
$idInfo = $oidc->validateIDToken($tokens['id_token'], $nonce);
// Get userinfo (includes groups because of scope).
$userInfo = $oidc->getUserInfo($tokens['access_token']);
// Store everything we need in the session.
$_SESSION['user'] = [
'id_token' => $tokens['id_token'],
'access_token'=> $tokens['access_token'],
'refresh_token'=> $tokens['refresh_token'] ?? null,
'expires_in' => $tokens['expires_in'] ?? null,
'id_info' => $idInfo,
'user_info' => $userInfo,
];
} catch (\Throwable $e) {
http_response_code(500);
exit('Error during OIDC exchange: ' . $e->getMessage());
}
// Clean up the query string (code, state) by redirecting to the home page.
header('Location: /');
exit;
-6
View File
@@ -1,6 +0,0 @@
{
"require": {
"guzzlehttp/guzzle": "^7.0",
"firebase/php-jwt": "^6.0"
}
}
-23
View File
@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
session_start();
/**
* Load environment variables. If you don't use phpdotenv,
* copy the values from .env directly here.
*/
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
/**
* Basic OIDC configuration – keep all secrets out of source‑control.
*/
define('OIDC_CLIENT_ID', getenv('OIDC_CLIENT_ID'));
define('OIDC_CLIENT_SECRET', getenv('OIDC_CLIENT_SECRET'));
define('OIDC_REDIRECT_URI', getenv('OIDC_REDIRECT_URI'));
define('OIDC_AUTH_ENDPOINT', getenv('OIDC_AUTH_ENDPOINT'));
define('OIDC_TOKEN_ENDPOINT', getenv('OIDC_TOKEN_ENDPOINT'));
define('OIDC_USERINFO_ENDPOINT', getenv('OIDC_USERINFO_ENDPOINT'));
define('OIDC_LOGOUT_ENDPOINT', getenv('OIDC_LOGOUT_ENDPOINT'));
define('OIDC_POST_LOGOUT_REDIRECT_URI', getenv('OIDC_POST_LOGOUT_REDIRECT_URI'));
-38
View File
@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fitness Site – OIDC Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 2rem; }
.info { margin-top: 1rem; }
pre { background: #f4f4f4; padding: 1rem; }
</style>
</head>
<body>
<h1>Fitness Site – OIDC Demo</h1>
<?php if (!isset($_SESSION['user'])): ?>
<p>You are not logged in.</p>
<p><a href="/login.php"><button>Login with Authelia</button></a></p>
<?php else: ?>
<p>Logged in as: <strong><?=htmlspecialchars($_SESSION['user']['user_info']['name'] ?? 'N/A')?></strong></p>
<div class="info">
<h2>User Information</h2>
<pre><?=htmlspecialchars(json_encode($_SESSION['user']['user_info'], JSON_PRETTY_PRINT))?></pre>
</div>
<div class="info">
<h2>ID Token Payload (claims)</h2>
<pre><?=htmlspecialchars(json_encode($_SESSION['user']['id_info'], JSON_PRETTY_PRINT))?></pre>
</div>
<p><a href="/logout.php"><button>Logout</button></a></p>
<?php endif; ?>
</body>
</html>
-18
View File
@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/vendor/autoload.php';
use Fitness\OIDC\OIDC;
// Generate a cryptographically random state & nonce.
$state = bin2hex(random_bytes(16));
$nonce = bin2hex(random_bytes(16));
// Persist them in the session to validate later.
$_SESSION['oidc_state'] = $state;
$_SESSION['oidc_nonce'] = $nonce;
$oidc = new OIDC();
header('Location: ' . $oidc->buildAuthUrl($state, $nonce));
exit;
-15
View File
@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
// Destroy the PHP session.
session_start();
session_destroy();
// Redirect the user to Authelia’s logout endpoint.
// Authelia will clean its session and then redirect back.
$logoutUrl = OIDC_LOGOUT_ENDPOINT
. '?post_logout_redirect_uri=' . urlencode(OIDC_POST_LOGOUT_REDIRECT_URI);
header('Location: ' . $logoutUrl);
exit;
-91
View File
@@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
namespace Fitness\OIDC;
use GuzzleHttp\Client;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class OIDC
{
private Client $http;
public function __construct()
{
$this->http = new Client(['http_errors' => false]);
}
/**
* Build the Auth URL that redirects the user to Authelia.
*/
public function buildAuthUrl(string $state, string $nonce): string
{
$params = [
'response_type' => 'code',
'client_id' => OIDC_CLIENT_ID,
'redirect_uri' => OIDC_REDIRECT_URI,
'scope' => 'openid profile email groups',
'state' => $state,
'nonce' => $nonce,
];
return OIDC_AUTH_ENDPOINT . '?' . http_build_query($params);
}
/**
* Exchange the Authorization Code for tokens.
*/
public function exchangeCode(string $code): array
{
$response = $this->http->post(OIDC_TOKEN_ENDPOINT, [
'auth' => [OIDC_CLIENT_ID, OIDC_CLIENT_SECRET],
'form_params' => [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => OIDC_REDIRECT_URI,
],
]);
if ($response->getStatusCode() !== 200) {
throw new \RuntimeException('Token endpoint returned ' . $response->getStatusCode());
}
return json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR);
}
/**
* Call /userinfo with the access token.
*/
public function getUserInfo(string $accessToken): array
{
$response = $this->http->get(OIDC_USERINFO_ENDPOINT, [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
],
]);
if ($response->getStatusCode() !== 200) {
throw new \RuntimeException('Userinfo endpoint returned ' . $response->getStatusCode());
}
return json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR);
}
/**
* Validate the ID Token (minimal validation for demo purposes).
*/
public function validateIDToken(string $idToken, string $nonce): array
{
// Authelia uses RS256 by default, but your config says "none".
// We'll decode without verification if alg is "none".
$decoded = JWT::decode($idToken, new Key('', ''), ['none', 'RS256']);
// Check nonce matches what we stored in session.
if ($decoded->nonce !== $nonce) {
throw new \RuntimeException('Invalid nonce');
}
return (array)$decoded;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<?php <?php
require_once __DIR__.'/config.php'; require_once __DIR__ . '/../fitness-config.php';
function uploadPhotos($provider_id, $files) { function uploadPhotos($provider_id, $files) {
$url = BASE_API_URL . "/api/providers/$provider_id/photos"; $url = BASE_API_URL . "/api/providers/$provider_id/photos";
-114
View File
@@ -1,114 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fitness Tracker</title>
<style>
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; }
th { background:#f0f0f0; }
#drop-zone { border: 2px dashed #aaa; padding: 1rem; text-align:center; cursor:pointer; }
#drop-zone.hover { background:#e0e0ff; }
</style>
</head>
<body>
<h1>Providers</h1>
<button id="add-provider-btn">Add Provider</button>
<table id="providers-table">
<thead><tr><th>ID</th><th>Name</th><th>Phone</th><th>Photo</th></tr></thead>
<tbody></tbody>
</table>
<!-- Add Provider Modal -->
<div id="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5);">
<div style="background:#fff; margin:5% auto; padding:1rem; width:300px; position:relative;">
<h2>Add Provider</h2>
<label>Name: <input type="text" id="p-name"></label><br>
<label>Age: <input type="number" id="p-age"></label><br>
<label>Phone: <input type="text" id="p-phone"></label><br>
<label>Rates: <input type="text" id="p-rates" placeholder='{"hourly":70}'></label><br>
<label>Private: <input type="checkbox" id="p-private"></label><br>
<div id="drop-zone">Drag photo or click to upload</div>
<input type="file" id="p-file" style="display:none;">
<br>
<button id="save-provider">Save</button>
<button id="cancel-provider">Cancel</button>
</div>
</div>
<script>
const API = 'http://localhost:5000';
// ----------- Fetch & render providers -----------------
async function loadProviders() {
const res = await fetch(`${API}/providers`);
const data = await res.json();
const tbody = document.querySelector('#providers-table tbody');
tbody.innerHTML = '';
data.forEach(p => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${p.id}</td>
<td>${p.name}</td>
<td>${p.phone}</td>
<td><img src="${p.photo}" width="50"></td>
`;
tbody.appendChild(tr);
});
}
loadProviders();
// ----------- Modal handling -----------------
const modal = document.getElementById('modal');
const addBtn = document.getElementById('add-provider-btn');
const cancelBtn = document.getElementById('cancel-provider');
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('p-file');
let uploadedPhotoUrl = null;
addBtn.onclick = () => { modal.style.display='block'; }
cancelBtn.onclick = () => { modal.style.display='none'; }
dropZone.onclick = () => fileInput.click();
fileInput.onchange = () => uploadFile(fileInput.files[0]);
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('hover'); });
dropZone.addEventListener('dragleave', e => { e.preventDefault(); dropZone.classList.remove('hover'); });
dropZone.addEventListener('drop', e => {
e.preventDefault(); dropZone.classList.remove('hover');
uploadFile(e.dataTransfer.files[0]);
});
async function uploadFile(file) {
const formData = new FormData();
formData.append('photo', file);
const resp = await fetch(`${API}/upload_photo`, { method: 'POST', body: formData });
uploadedPhotoUrl = await resp.text(); // /uploads/xxxx.jpg
dropZone.textContent = '✓ Uploaded';
}
// ----------- Save provider -----------------
const saveBtn = document.getElementById('save-provider');
saveBtn.onclick = async () => {
const payload = {
name: document.getElementById('p-name').value,
age: parseInt(document.getElementById('p-age').value, 10),
phone: document.getElementById('p-phone').value,
rates: JSON.parse(document.getElementById('p-rates').value),
private: document.getElementById('p-private').checked,
photo: uploadedPhotoUrl
};
await fetch(`${API}/providers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
modal.style.display='none';
loadProviders(); // refresh table
};
</script>
</body>
</html>