Compare commits
34
Commits
965d2d084a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9a2dd109 | ||
|
|
fc96702600 | ||
|
|
efbfbe8441 | ||
|
|
9163437130 | ||
|
|
dd4b382d8c | ||
|
|
9dc05a72b5 | ||
|
|
9e4c6571cf | ||
|
|
2efd469f1e | ||
|
|
5ec16d4cbc | ||
|
|
49d2e818d0 | ||
|
|
ff8cf722f4 | ||
|
|
ca1e36b0b6 | ||
|
|
c5c1081589 | ||
|
|
0dd3e43893 | ||
|
|
07d3895eb5 | ||
|
|
4b7abac7ca | ||
|
|
e9bc19d5e2 | ||
|
|
124c5a1ff6 | ||
|
|
0cf83eb9d6 | ||
|
|
addf70b7bb | ||
|
|
221d435480 | ||
|
|
65a69b585d | ||
|
|
6f8dd7bbe3 | ||
|
|
933134e3b0 | ||
|
|
58de6d1073 | ||
|
|
ceb0ce1778 | ||
|
|
9ddcc36893 | ||
|
|
97a31db020 | ||
|
|
a759eb206f | ||
|
|
5cd6084c3c | ||
|
|
59e99b74a7 | ||
|
|
33644b69c3 | ||
|
|
f2810a7950 | ||
|
|
d572067cf6 |
No files matched your search
+17
-5
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"jumbojett/openid-connect-php": "^1.0",
|
||||||
|
"vlucas/phpdotenv": "^5.4",
|
||||||
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
|
"firebase/php-jwt": "^7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
Whitespace-only changes.
@@ -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/
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"require": {
|
|
||||||
"guzzlehttp/guzzle": "^7.0",
|
|
||||||
"firebase/php-jwt": "^6.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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'));
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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,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
@@ -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>
|
|
||||||
Reference in new issue
Block a user