added bootstar, font awsome, and the project is in a useable state, bit needs some manual setup.
This commit is contained in:
500
ProjectKiln/auth.php
Normal file
500
ProjectKiln/auth.php
Normal file
@@ -0,0 +1,500 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
const REMEMBER_COOKIE_NAME = 'remember_login';
|
||||
const API_TOKEN_COOKIE_NAME = 'api_token';
|
||||
|
||||
const REMEMBER_DAYS = 30;
|
||||
const AUTH_TOKEN_HOURS = 12;
|
||||
|
||||
function isSecureRequest(): bool
|
||||
{
|
||||
return (
|
||||
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|| (($_SERVER['SERVER_PORT'] ?? null) === '443')
|
||||
);
|
||||
}
|
||||
|
||||
function hashPassword(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_ARGON2ID);
|
||||
}
|
||||
|
||||
function setApiTokenCookie(string $token): void
|
||||
{
|
||||
setcookie(API_TOKEN_COOKIE_NAME, $token, [
|
||||
'expires' => time() + 60 * 60 * AUTH_TOKEN_HOURS,
|
||||
'path' => '/',
|
||||
'secure' => isSecureRequest(),
|
||||
'httponly' => false,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
$_COOKIE[API_TOKEN_COOKIE_NAME] = $token;
|
||||
}
|
||||
|
||||
function deleteApiTokenCookie(): void
|
||||
{
|
||||
setcookie(API_TOKEN_COOKIE_NAME, '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => isSecureRequest(),
|
||||
'httponly' => false,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
unset($_COOKIE[API_TOKEN_COOKIE_NAME]);
|
||||
}
|
||||
|
||||
function login(string $email, string $password, bool $remember = false): ?string
|
||||
{
|
||||
$email = strtolower(trim($email));
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM users WHERE email = ? LIMIT 1');
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($password, $user['passwd'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = (int)$user['id'];
|
||||
|
||||
if ($remember) {
|
||||
createRememberToken((int)$user['id']);
|
||||
}
|
||||
|
||||
$token = createAuthToken((int)$user['id']);
|
||||
setApiTokenCookie($token);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
function logout(): void
|
||||
{
|
||||
$token = getBearerToken();
|
||||
|
||||
if ($token) {
|
||||
revokeAuthToken($token);
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
revokeUserAuthTokens((int)$_SESSION['user_id']);
|
||||
}
|
||||
|
||||
deleteRememberCookie();
|
||||
deleteApiTokenCookie();
|
||||
|
||||
$_SESSION = [];
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function currentUser(): ?array
|
||||
{
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
tryRememberLogin();
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$stmt = db()->prepare(
|
||||
'SELECT id, name, email, picture
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
LIMIT 1'
|
||||
);
|
||||
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
$token = getBearerToken();
|
||||
|
||||
if ($token) {
|
||||
return authenticateApiToken($token);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function requireLogin(): array
|
||||
{
|
||||
$user = currentUser();
|
||||
|
||||
if (!$user) {
|
||||
header('Location: ?page=login');
|
||||
exit;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function requireGuest(): void
|
||||
{
|
||||
if (currentUser()) {
|
||||
header('Location: ?page=home');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function createRememberToken(int $userId): void
|
||||
{
|
||||
$selector = bin2hex(random_bytes(16));
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
$expires = date('Y-m-d H:i:s', time() + 60 * 60 * 24 * REMEMBER_DAYS);
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO remember_tokens (user_id, selector, token_hash, expires_at)
|
||||
VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
|
||||
$stmt->execute([$userId, $selector, $tokenHash, $expires]);
|
||||
|
||||
setcookie(REMEMBER_COOKIE_NAME, $selector . ':' . $token, [
|
||||
'expires' => time() + 60 * 60 * 24 * REMEMBER_DAYS,
|
||||
'path' => '/',
|
||||
'secure' => isSecureRequest(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
|
||||
function tryRememberLogin(): void
|
||||
{
|
||||
if (empty($_COOKIE[REMEMBER_COOKIE_NAME])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parts = explode(':', $_COOKIE[REMEMBER_COOKIE_NAME]);
|
||||
|
||||
if (count($parts) !== 2) {
|
||||
deleteRememberCookie();
|
||||
return;
|
||||
}
|
||||
|
||||
[$selector, $token] = $parts;
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'SELECT *
|
||||
FROM remember_tokens
|
||||
WHERE selector = ?
|
||||
AND expires_at > NOW()
|
||||
LIMIT 1'
|
||||
);
|
||||
|
||||
$stmt->execute([$selector]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row || !hash_equals($row['token_hash'], $tokenHash)) {
|
||||
deleteRememberCookie();
|
||||
return;
|
||||
}
|
||||
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = (int)$row['user_id'];
|
||||
|
||||
db()->prepare('DELETE FROM remember_tokens WHERE id = ?')->execute([$row['id']]);
|
||||
|
||||
createRememberToken((int)$row['user_id']);
|
||||
|
||||
$apiToken = createAuthToken((int)$row['user_id']);
|
||||
setApiTokenCookie($apiToken);
|
||||
}
|
||||
|
||||
function deleteRememberCookie(): void
|
||||
{
|
||||
if (!empty($_COOKIE[REMEMBER_COOKIE_NAME])) {
|
||||
$parts = explode(':', $_COOKIE[REMEMBER_COOKIE_NAME]);
|
||||
|
||||
if (count($parts) === 2) {
|
||||
db()->prepare(
|
||||
'DELETE FROM remember_tokens WHERE selector = ?'
|
||||
)->execute([$parts[0]]);
|
||||
}
|
||||
}
|
||||
|
||||
setcookie(REMEMBER_COOKIE_NAME, '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => isSecureRequest(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
unset($_COOKIE[REMEMBER_COOKIE_NAME]);
|
||||
}
|
||||
|
||||
function createAuthToken(int $userId): string
|
||||
{
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
$expires = date('Y-m-d H:i:s', time() + 60 * 60 * AUTH_TOKEN_HOURS);
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO auth_tokens (user_id, token_hash, expires_at)
|
||||
VALUES (?, ?, ?)'
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
$userId,
|
||||
$tokenHash,
|
||||
$expires
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
function authenticateApiToken(string $token): ?array
|
||||
{
|
||||
$token = trim($token);
|
||||
|
||||
if ($token === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'SELECT users.id, users.name, users.email, users.picture
|
||||
FROM auth_tokens
|
||||
JOIN users ON users.id = auth_tokens.user_id
|
||||
WHERE auth_tokens.token_hash = ?
|
||||
AND auth_tokens.expires_at > NOW()
|
||||
AND auth_tokens.revoked_at IS NULL
|
||||
LIMIT 1'
|
||||
);
|
||||
|
||||
$stmt->execute([$tokenHash]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
db()->prepare(
|
||||
'UPDATE auth_tokens
|
||||
SET last_used_at = NOW()
|
||||
WHERE token_hash = ?'
|
||||
)->execute([$tokenHash]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function getBearerToken(): ?string
|
||||
{
|
||||
$header = $_SERVER['HTTP_AUTHORIZATION']
|
||||
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
|
||||
?? '';
|
||||
|
||||
if (preg_match('/Bearer\s+(.+)/i', $header, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
if (!empty($_COOKIE[API_TOKEN_COOKIE_NAME])) {
|
||||
return trim($_COOKIE[API_TOKEN_COOKIE_NAME]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function jsonAuthError(string $message, int $status): void
|
||||
{
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $message
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
function userHasRight(int $userId, string $rightName): bool
|
||||
{
|
||||
try {
|
||||
$stmt = db()->prepare(
|
||||
'SELECT rights.id
|
||||
FROM user_rights
|
||||
INNER JOIN rights ON rights.id = user_rights.right_id
|
||||
WHERE user_rights.user_id = ?
|
||||
AND rights.name = ?
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([$userId, $rightName]);
|
||||
|
||||
return (bool)$stmt->fetch();
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function userIsAdmin(int $userId): bool
|
||||
{
|
||||
return userHasRight($userId, 'Admin');
|
||||
}
|
||||
|
||||
function requireUserRight(int $userId, string $rightName): void
|
||||
{
|
||||
if (userIsAdmin($userId) || userHasRight($userId, $rightName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
jsonAuthError("Missing right: {$rightName}.", 403);
|
||||
}
|
||||
|
||||
function projectExists(string $projectId): bool
|
||||
{
|
||||
$stmt = db()->prepare('SELECT id FROM projects WHERE id = ? LIMIT 1');
|
||||
$stmt->execute([$projectId]);
|
||||
|
||||
return (bool)$stmt->fetch();
|
||||
}
|
||||
|
||||
function userCanAccessProject(int $userId, string $projectId): bool
|
||||
{
|
||||
if (userIsAdmin($userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'SELECT projects.id
|
||||
FROM projects
|
||||
LEFT JOIN user_access
|
||||
ON user_access.project_id = projects.id
|
||||
AND user_access.user_id = ?
|
||||
WHERE projects.id = ?
|
||||
AND (projects.owner = ? OR user_access.id IS NOT NULL)
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([$userId, $projectId, $userId]);
|
||||
|
||||
return (bool)$stmt->fetch();
|
||||
}
|
||||
|
||||
function requireProjectAccessForUser(int $userId, string $projectId): void
|
||||
{
|
||||
if (!projectExists($projectId)) {
|
||||
jsonAuthError('Project not found.', 404);
|
||||
}
|
||||
|
||||
if (!userCanAccessProject($userId, $projectId)) {
|
||||
jsonAuthError('No access to this project.', 403);
|
||||
}
|
||||
}
|
||||
|
||||
function requireProjectRight(int $userId, string $projectId, string $rightName): void
|
||||
{
|
||||
requireProjectAccessForUser($userId, $projectId);
|
||||
requireUserRight($userId, $rightName);
|
||||
}
|
||||
|
||||
function requireApiAuth(): array
|
||||
{
|
||||
$token = getBearerToken();
|
||||
|
||||
if (!$token) {
|
||||
jsonAuthError('Missing API token.', 401);
|
||||
}
|
||||
|
||||
$user = authenticateApiToken($token);
|
||||
|
||||
if (!$user) {
|
||||
jsonAuthError('Invalid or expired API token.', 401);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function revokeAuthToken(string $token): void
|
||||
{
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
db()->prepare(
|
||||
'UPDATE auth_tokens
|
||||
SET revoked_at = NOW()
|
||||
WHERE token_hash = ?
|
||||
AND revoked_at IS NULL'
|
||||
)->execute([$tokenHash]);
|
||||
}
|
||||
|
||||
function revokeUserAuthTokens(int $userId): void
|
||||
{
|
||||
db()->prepare(
|
||||
'UPDATE auth_tokens
|
||||
SET revoked_at = NOW()
|
||||
WHERE user_id = ?
|
||||
AND revoked_at IS NULL'
|
||||
)->execute([$userId]);
|
||||
}
|
||||
|
||||
function cleanupExpiredAuthData(): void
|
||||
{
|
||||
db()->exec('DELETE FROM remember_tokens WHERE expires_at < NOW()');
|
||||
db()->exec('DELETE FROM auth_tokens WHERE expires_at < NOW() OR revoked_at IS NOT NULL');
|
||||
}
|
||||
|
||||
function register(string $username, string $email, string $password): ?int
|
||||
{
|
||||
$username = trim($username);
|
||||
$email = strtolower(trim($email));
|
||||
|
||||
if ($username === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($email === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($password === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$passwordHash = password_hash($password, PASSWORD_ARGON2ID);
|
||||
|
||||
if ($passwordHash === false) {
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO users (name, email, passwd)
|
||||
VALUES (?, ?, ?)'
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
$username,
|
||||
$email,
|
||||
$passwordHash
|
||||
]);
|
||||
|
||||
return (int)db()->lastInsertId();
|
||||
|
||||
} catch (PDOException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user