first pass on auth
This commit is contained in:
parent
2e181738f3
commit
dfae8871e1
319
auth/Auth.php
Normal file
319
auth/Auth.php
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/UserProviderInterface.php';
|
||||||
|
require_once __DIR__ . '/User.php';
|
||||||
|
require_once __DIR__ . '/../session.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth handles user authentication with cookie support
|
||||||
|
*/
|
||||||
|
class Auth
|
||||||
|
{
|
||||||
|
private UserProviderInterface $provider;
|
||||||
|
private Session $session;
|
||||||
|
private ?User $user = null;
|
||||||
|
private array $config;
|
||||||
|
|
||||||
|
const SESSION_KEY = 'auth_user_id';
|
||||||
|
const REMEMBER_COOKIE = 'remember_token';
|
||||||
|
const REMEMBER_DURATION = 2592000; // 30 days in seconds
|
||||||
|
|
||||||
|
public function __construct(UserProviderInterface $provider, Session $session, array $config = [])
|
||||||
|
{
|
||||||
|
$this->provider = $provider;
|
||||||
|
$this->session = $session;
|
||||||
|
$this->config = array_merge([
|
||||||
|
'cookie_name' => self::REMEMBER_COOKIE,
|
||||||
|
'cookie_lifetime' => self::REMEMBER_DURATION,
|
||||||
|
'cookie_path' => '/',
|
||||||
|
'cookie_domain' => '',
|
||||||
|
'cookie_secure' => false,
|
||||||
|
'cookie_httponly' => true,
|
||||||
|
'cookie_samesite' => 'Lax'
|
||||||
|
], $config);
|
||||||
|
|
||||||
|
$this->initializeFromSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate user with credentials
|
||||||
|
*/
|
||||||
|
public function attempt(array $credentials, bool $remember = false): bool
|
||||||
|
{
|
||||||
|
$identifier = $credentials['email'] ?? $credentials['username'] ?? null;
|
||||||
|
$password = $credentials['password'] ?? null;
|
||||||
|
|
||||||
|
if (!$identifier || !$password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->provider->findByCredentials($identifier);
|
||||||
|
|
||||||
|
if (!$user || !$this->provider->verifyPassword($user, $password)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->login($user, $remember);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login a user instance
|
||||||
|
*/
|
||||||
|
public function login(User $user, bool $remember = false): void
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->session->set(self::SESSION_KEY, $user->getId());
|
||||||
|
$this->session->regenerate();
|
||||||
|
|
||||||
|
if ($remember) {
|
||||||
|
$this->createRememberToken($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login using user ID
|
||||||
|
*/
|
||||||
|
public function loginById(int|string $id, bool $remember = false): bool
|
||||||
|
{
|
||||||
|
$user = $this->provider->findById($id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->login($user, $remember);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout the current user
|
||||||
|
*/
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
if ($this->user && $this->user->rememberToken) {
|
||||||
|
$this->provider->updateRememberToken($this->user, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->user = null;
|
||||||
|
$this->session->remove(self::SESSION_KEY);
|
||||||
|
$this->session->regenerate();
|
||||||
|
$this->clearRememberCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is authenticated
|
||||||
|
*/
|
||||||
|
public function check(): bool
|
||||||
|
{
|
||||||
|
return $this->user() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is guest (not authenticated)
|
||||||
|
*/
|
||||||
|
public function guest(): bool
|
||||||
|
{
|
||||||
|
return !$this->check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently authenticated user
|
||||||
|
*/
|
||||||
|
public function user(): ?User
|
||||||
|
{
|
||||||
|
if ($this->user) {
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load from session
|
||||||
|
$userId = $this->session->get(self::SESSION_KEY);
|
||||||
|
if ($userId) {
|
||||||
|
$this->user = $this->provider->findById($userId);
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load from remember cookie
|
||||||
|
$this->user = $this->getUserFromRememberCookie();
|
||||||
|
if ($this->user) {
|
||||||
|
$this->session->set(self::SESSION_KEY, $this->user->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user ID
|
||||||
|
*/
|
||||||
|
public function id(): int|string|null
|
||||||
|
{
|
||||||
|
return $this->user()?->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new user
|
||||||
|
*/
|
||||||
|
public function register(array $data, bool $login = true): User
|
||||||
|
{
|
||||||
|
// Hash password before storing
|
||||||
|
if (isset($data['password'])) {
|
||||||
|
$data['password'] = $this->hashPassword($data['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->provider->create($data);
|
||||||
|
|
||||||
|
if ($login) {
|
||||||
|
$this->login($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate user credentials without logging in
|
||||||
|
*/
|
||||||
|
public function validate(array $credentials): bool
|
||||||
|
{
|
||||||
|
$identifier = $credentials['email'] ?? $credentials['username'] ?? null;
|
||||||
|
$password = $credentials['password'] ?? null;
|
||||||
|
|
||||||
|
if (!$identifier || !$password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->provider->findByCredentials($identifier);
|
||||||
|
|
||||||
|
return $user && $this->provider->verifyPassword($user, $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password
|
||||||
|
*/
|
||||||
|
public function hashPassword(string $password): string
|
||||||
|
{
|
||||||
|
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a password against hash
|
||||||
|
*/
|
||||||
|
public function verifyPassword(string $password, string $hash): bool
|
||||||
|
{
|
||||||
|
return password_verify($password, $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize user from session
|
||||||
|
*/
|
||||||
|
private function initializeFromSession(): void
|
||||||
|
{
|
||||||
|
if (!$this->session->isStarted()) {
|
||||||
|
$this->session->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->user();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create remember token for user
|
||||||
|
*/
|
||||||
|
private function createRememberToken(User $user): void
|
||||||
|
{
|
||||||
|
$token = $this->generateRememberToken();
|
||||||
|
$hashedToken = hash('sha256', $token);
|
||||||
|
|
||||||
|
$this->provider->updateRememberToken($user, $hashedToken);
|
||||||
|
$user->rememberToken = $hashedToken;
|
||||||
|
|
||||||
|
$this->setRememberCookie($user->getId() . '|' . $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random remember token
|
||||||
|
*/
|
||||||
|
private function generateRememberToken(): string
|
||||||
|
{
|
||||||
|
return bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set remember cookie
|
||||||
|
*/
|
||||||
|
private function setRememberCookie(string $value): void
|
||||||
|
{
|
||||||
|
setcookie(
|
||||||
|
$this->config['cookie_name'],
|
||||||
|
$value,
|
||||||
|
[
|
||||||
|
'expires' => time() + $this->config['cookie_lifetime'],
|
||||||
|
'path' => $this->config['cookie_path'],
|
||||||
|
'domain' => $this->config['cookie_domain'],
|
||||||
|
'secure' => $this->config['cookie_secure'],
|
||||||
|
'httponly' => $this->config['cookie_httponly'],
|
||||||
|
'samesite' => $this->config['cookie_samesite']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear remember cookie
|
||||||
|
*/
|
||||||
|
private function clearRememberCookie(): void
|
||||||
|
{
|
||||||
|
setcookie(
|
||||||
|
$this->config['cookie_name'],
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
'expires' => time() - 3600,
|
||||||
|
'path' => $this->config['cookie_path'],
|
||||||
|
'domain' => $this->config['cookie_domain'],
|
||||||
|
'secure' => $this->config['cookie_secure'],
|
||||||
|
'httponly' => $this->config['cookie_httponly'],
|
||||||
|
'samesite' => $this->config['cookie_samesite']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user from remember cookie
|
||||||
|
*/
|
||||||
|
private function getUserFromRememberCookie(): ?User
|
||||||
|
{
|
||||||
|
$cookie = $_COOKIE[$this->config['cookie_name']] ?? null;
|
||||||
|
|
||||||
|
if (!$cookie || !str_contains($cookie, '|')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$id, $token] = explode('|', $cookie, 2);
|
||||||
|
$hashedToken = hash('sha256', $token);
|
||||||
|
|
||||||
|
$user = $this->provider->findById($id);
|
||||||
|
|
||||||
|
if (!$user || $user->rememberToken !== $hashedToken) {
|
||||||
|
$this->clearRememberCookie();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh user instance from provider
|
||||||
|
*/
|
||||||
|
public function refresh(): void
|
||||||
|
{
|
||||||
|
if ($this->user) {
|
||||||
|
$this->user = $this->provider->findById($this->user->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current user
|
||||||
|
*/
|
||||||
|
public function setUser(User $user): void
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->session->set(self::SESSION_KEY, $user->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
206
auth/AuthMiddleware.php
Normal file
206
auth/AuthMiddleware.php
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/Auth.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthMiddleware provides authentication checks for routes
|
||||||
|
*/
|
||||||
|
class AuthMiddleware
|
||||||
|
{
|
||||||
|
private Auth $auth;
|
||||||
|
|
||||||
|
public function __construct(Auth $auth)
|
||||||
|
{
|
||||||
|
$this->auth = $auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require authentication
|
||||||
|
*/
|
||||||
|
public function requireAuth(): callable
|
||||||
|
{
|
||||||
|
return function(Context $context, callable $next) {
|
||||||
|
if ($this->auth->guest()) {
|
||||||
|
// Check if request expects JSON
|
||||||
|
if ($context->request->expectsJson()) {
|
||||||
|
$context->json(['error' => 'Unauthenticated'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login page
|
||||||
|
$context->redirect('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user to context
|
||||||
|
$context->set('user', $this->auth->user());
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require guest (not authenticated)
|
||||||
|
*/
|
||||||
|
public function requireGuest(): callable
|
||||||
|
{
|
||||||
|
return function(Context $context, callable $next) {
|
||||||
|
if ($this->auth->check()) {
|
||||||
|
// Check if request expects JSON
|
||||||
|
if ($context->request->expectsJson()) {
|
||||||
|
$context->json(['error' => 'Already authenticated'], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to home or dashboard
|
||||||
|
$context->redirect('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional authentication (sets user if authenticated)
|
||||||
|
*/
|
||||||
|
public function optional(): callable
|
||||||
|
{
|
||||||
|
return function(Context $context, callable $next) {
|
||||||
|
if ($this->auth->check()) {
|
||||||
|
$context->set('user', $this->auth->user());
|
||||||
|
}
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has specific role
|
||||||
|
*/
|
||||||
|
public function requireRole(string|array $roles): callable
|
||||||
|
{
|
||||||
|
$roles = is_array($roles) ? $roles : [$roles];
|
||||||
|
|
||||||
|
return function(Context $context, callable $next) use ($roles) {
|
||||||
|
if ($this->auth->guest()) {
|
||||||
|
if ($context->request->expectsJson()) {
|
||||||
|
$context->json(['error' => 'Unauthenticated'], 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$context->redirect('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->auth->user();
|
||||||
|
$userRole = $user->getAttribute('role') ?? $user->role ?? null;
|
||||||
|
|
||||||
|
if (!in_array($userRole, $roles)) {
|
||||||
|
if ($context->request->expectsJson()) {
|
||||||
|
$context->json(['error' => 'Insufficient permissions'], 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$context->error(403, 'Forbidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->set('user', $user);
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiting per user
|
||||||
|
*/
|
||||||
|
public function rateLimit(int $maxAttempts = 60, int $decayMinutes = 1): callable
|
||||||
|
{
|
||||||
|
return function(Context $context, callable $next) use ($maxAttempts, $decayMinutes) {
|
||||||
|
if ($this->auth->guest()) {
|
||||||
|
$identifier = $context->request->ip();
|
||||||
|
} else {
|
||||||
|
$identifier = 'user:' . $this->auth->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = 'rate_limit:' . $identifier . ':' . $context->request->path;
|
||||||
|
$attempts = $context->session->get($key, 0);
|
||||||
|
$resetTime = $context->session->get($key . ':reset', 0);
|
||||||
|
|
||||||
|
// Reset counter if decay time has passed
|
||||||
|
if (time() > $resetTime) {
|
||||||
|
$attempts = 0;
|
||||||
|
$context->session->set($key . ':reset', time() + ($decayMinutes * 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($attempts >= $maxAttempts) {
|
||||||
|
$retryAfter = $resetTime - time();
|
||||||
|
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
|
||||||
|
$context->response->header('X-RateLimit-Remaining', '0');
|
||||||
|
$context->response->header('Retry-After', (string)$retryAfter);
|
||||||
|
|
||||||
|
if ($context->request->expectsJson()) {
|
||||||
|
$context->json([
|
||||||
|
'error' => 'Too many requests',
|
||||||
|
'retry_after' => $retryAfter
|
||||||
|
], 429);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->error(429, 'Too Many Requests');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment attempts
|
||||||
|
$context->session->set($key, $attempts + 1);
|
||||||
|
|
||||||
|
// Add rate limit headers
|
||||||
|
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
|
||||||
|
$context->response->header('X-RateLimit-Remaining', (string)($maxAttempts - $attempts - 1));
|
||||||
|
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSRF protection
|
||||||
|
*/
|
||||||
|
public function verifyCsrf(): callable
|
||||||
|
{
|
||||||
|
return function(Context $context, callable $next) {
|
||||||
|
// Skip CSRF for safe methods
|
||||||
|
if (in_array($context->request->method, ['GET', 'HEAD', 'OPTIONS'])) {
|
||||||
|
$next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $context->request->input('_token')
|
||||||
|
?? $context->request->header('X-CSRF-TOKEN')
|
||||||
|
?? $context->request->header('X-XSRF-TOKEN');
|
||||||
|
|
||||||
|
if (!$context->session->validateToken($token)) {
|
||||||
|
if ($context->request->expectsJson()) {
|
||||||
|
$context->json(['error' => 'CSRF token mismatch'], 419);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$context->error(419, 'CSRF token mismatch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remember user from cookie
|
||||||
|
*/
|
||||||
|
public function remember(): callable
|
||||||
|
{
|
||||||
|
return function(Context $context, callable $next) {
|
||||||
|
// Auth class already handles remember cookies in constructor
|
||||||
|
// This middleware can be used to refresh the remember token if needed
|
||||||
|
|
||||||
|
if ($this->auth->check()) {
|
||||||
|
$context->set('user', $this->auth->user());
|
||||||
|
}
|
||||||
|
|
||||||
|
$next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
83
auth/User.php
Normal file
83
auth/User.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User model represents an authenticated user
|
||||||
|
*/
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
public int|string $id;
|
||||||
|
public string $username;
|
||||||
|
public string $email;
|
||||||
|
public string $password;
|
||||||
|
public ?string $rememberToken;
|
||||||
|
public array $attributes = [];
|
||||||
|
public ?string $createdAt;
|
||||||
|
public ?string $updatedAt;
|
||||||
|
|
||||||
|
public function __construct(array $data = [])
|
||||||
|
{
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if (property_exists($this, $key)) {
|
||||||
|
$this->$key = $value;
|
||||||
|
} else {
|
||||||
|
$this->attributes[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user identifier (email or username)
|
||||||
|
*/
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->email ?: $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user ID
|
||||||
|
*/
|
||||||
|
public function getId(): int|string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom attribute
|
||||||
|
*/
|
||||||
|
public function getAttribute(string $key): mixed
|
||||||
|
{
|
||||||
|
return $this->attributes[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom attribute
|
||||||
|
*/
|
||||||
|
public function setAttribute(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->attributes[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to array
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'username' => $this->username,
|
||||||
|
'email' => $this->email,
|
||||||
|
'created_at' => $this->createdAt,
|
||||||
|
'updated_at' => $this->updatedAt,
|
||||||
|
] + $this->attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to safe array (without sensitive data)
|
||||||
|
*/
|
||||||
|
public function toSafeArray(): array
|
||||||
|
{
|
||||||
|
$data = $this->toArray();
|
||||||
|
unset($data['password'], $data['remember_token']);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
auth/UserProviderInterface.php
Normal file
52
auth/UserProviderInterface.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserProviderInterface defines contract for user storage providers
|
||||||
|
*/
|
||||||
|
interface UserProviderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Find user by ID
|
||||||
|
*/
|
||||||
|
public function findById(int|string $id): ?User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by email
|
||||||
|
*/
|
||||||
|
public function findByEmail(string $email): ?User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by username
|
||||||
|
*/
|
||||||
|
public function findByUsername(string $username): ?User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by credentials (email or username)
|
||||||
|
*/
|
||||||
|
public function findByCredentials(string $identifier): ?User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by remember token
|
||||||
|
*/
|
||||||
|
public function findByRememberToken(string $token): ?User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
*/
|
||||||
|
public function create(array $data): User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user data
|
||||||
|
*/
|
||||||
|
public function update(User $user, array $data): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update remember token
|
||||||
|
*/
|
||||||
|
public function updateRememberToken(User $user, ?string $token): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify user password
|
||||||
|
*/
|
||||||
|
public function verifyPassword(User $user, string $password): bool;
|
||||||
|
}
|
||||||
170
auth/providers/JsonUserProvider.php
Normal file
170
auth/providers/JsonUserProvider.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../UserProviderInterface.php';
|
||||||
|
require_once __DIR__ . '/../User.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JsonUserProvider stores users in a JSON file
|
||||||
|
*/
|
||||||
|
class JsonUserProvider implements UserProviderInterface
|
||||||
|
{
|
||||||
|
private string $filePath;
|
||||||
|
private array $users = [];
|
||||||
|
|
||||||
|
public function __construct(string $filePath)
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
$this->loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load users from JSON file
|
||||||
|
*/
|
||||||
|
private function loadUsers(): void
|
||||||
|
{
|
||||||
|
if (!file_exists($this->filePath)) {
|
||||||
|
$this->users = [];
|
||||||
|
$this->saveUsers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($this->filePath);
|
||||||
|
$data = json_decode($content, true) ?? [];
|
||||||
|
|
||||||
|
$this->users = [];
|
||||||
|
foreach ($data as $userData) {
|
||||||
|
$this->users[$userData['id']] = new User($userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save users to JSON file
|
||||||
|
*/
|
||||||
|
private function saveUsers(): void
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
foreach ($this->users as $user) {
|
||||||
|
$userData = [
|
||||||
|
'id' => $user->id,
|
||||||
|
'username' => $user->username,
|
||||||
|
'email' => $user->email,
|
||||||
|
'password' => $user->password,
|
||||||
|
'remember_token' => $user->rememberToken,
|
||||||
|
'created_at' => $user->createdAt,
|
||||||
|
'updated_at' => $user->updatedAt,
|
||||||
|
] + $user->attributes;
|
||||||
|
$data[] = $userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($this->filePath, json_encode($data, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate next user ID
|
||||||
|
*/
|
||||||
|
private function generateId(): int
|
||||||
|
{
|
||||||
|
if (empty($this->users)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return max(array_keys($this->users)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findById(int|string $id): ?User
|
||||||
|
{
|
||||||
|
return $this->users[$id] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByEmail(string $email): ?User
|
||||||
|
{
|
||||||
|
foreach ($this->users as $user) {
|
||||||
|
if ($user->email === $email) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUsername(string $username): ?User
|
||||||
|
{
|
||||||
|
foreach ($this->users as $user) {
|
||||||
|
if ($user->username === $username) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByCredentials(string $identifier): ?User
|
||||||
|
{
|
||||||
|
// Check if identifier is email
|
||||||
|
if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
return $this->findByEmail($identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise treat as username
|
||||||
|
return $this->findByUsername($identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRememberToken(string $token): ?User
|
||||||
|
{
|
||||||
|
foreach ($this->users as $user) {
|
||||||
|
if ($user->rememberToken === $token) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): User
|
||||||
|
{
|
||||||
|
$data['id'] = $data['id'] ?? $this->generateId();
|
||||||
|
$data['created_at'] = $data['created_at'] ?? date('Y-m-d H:i:s');
|
||||||
|
$data['updated_at'] = $data['updated_at'] ?? date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$user = new User($data);
|
||||||
|
$this->users[$user->id] = $user;
|
||||||
|
$this->saveUsers();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, array $data): bool
|
||||||
|
{
|
||||||
|
if (!isset($this->users[$user->id])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if (property_exists($user, $key)) {
|
||||||
|
$user->$key = $value;
|
||||||
|
} else {
|
||||||
|
$user->attributes[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->updatedAt = date('Y-m-d H:i:s');
|
||||||
|
$this->users[$user->id] = $user;
|
||||||
|
$this->saveUsers();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRememberToken(User $user, ?string $token): bool
|
||||||
|
{
|
||||||
|
if (!isset($this->users[$user->id])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->rememberToken = $token;
|
||||||
|
$this->users[$user->id] = $user;
|
||||||
|
$this->saveUsers();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyPassword(User $user, string $password): bool
|
||||||
|
{
|
||||||
|
return password_verify($password, $user->password);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
auth/providers/MysqlUserProvider.php
Normal file
205
auth/providers/MysqlUserProvider.php
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../UserProviderInterface.php';
|
||||||
|
require_once __DIR__ . '/../User.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MysqlUserProvider stores users in MySQL database
|
||||||
|
*/
|
||||||
|
class MysqlUserProvider implements UserProviderInterface
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
private string $table;
|
||||||
|
|
||||||
|
public function __construct(array $config, string $table = 'users')
|
||||||
|
{
|
||||||
|
$this->table = $table;
|
||||||
|
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||||
|
$config['host'] ?? 'localhost',
|
||||||
|
$config['port'] ?? 3306,
|
||||||
|
$config['database'],
|
||||||
|
$config['charset'] ?? 'utf8mb4'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->db = new PDO(
|
||||||
|
$dsn,
|
||||||
|
$config['username'],
|
||||||
|
$config['password'],
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->createTable();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new Exception("Failed to connect to MySQL database: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create users table if it doesn't exist
|
||||||
|
*/
|
||||||
|
private function createTable(): void
|
||||||
|
{
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) UNIQUE,
|
||||||
|
email VARCHAR(255) UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
remember_token VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
attributes JSON,
|
||||||
|
INDEX idx_email (email),
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_remember_token (remember_token)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||||
|
|
||||||
|
$this->db->exec($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map database row to User object
|
||||||
|
*/
|
||||||
|
private function mapToUser(array $row): User
|
||||||
|
{
|
||||||
|
$attributes = isset($row['attributes']) ? json_decode($row['attributes'], true) ?? [] : [];
|
||||||
|
|
||||||
|
return new User([
|
||||||
|
'id' => $row['id'],
|
||||||
|
'username' => $row['username'],
|
||||||
|
'email' => $row['email'],
|
||||||
|
'password' => $row['password'],
|
||||||
|
'rememberToken' => $row['remember_token'],
|
||||||
|
'createdAt' => $row['created_at'],
|
||||||
|
'updatedAt' => $row['updated_at'],
|
||||||
|
'attributes' => $attributes
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findById(int|string $id): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByEmail(string $email): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE email = :email");
|
||||||
|
$stmt->execute(['email' => $email]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUsername(string $username): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE username = :username");
|
||||||
|
$stmt->execute(['username' => $username]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByCredentials(string $identifier): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"SELECT * FROM {$this->table} WHERE email = :identifier OR username = :identifier"
|
||||||
|
);
|
||||||
|
$stmt->execute(['identifier' => $identifier]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRememberToken(string $token): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE remember_token = :token");
|
||||||
|
$stmt->execute(['token' => $token]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): User
|
||||||
|
{
|
||||||
|
$attributes = $data['attributes'] ?? [];
|
||||||
|
unset($data['attributes']);
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"INSERT INTO {$this->table} (username, email, password, remember_token, attributes)
|
||||||
|
VALUES (:username, :email, :password, :remember_token, :attributes)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'username' => $data['username'] ?? null,
|
||||||
|
'email' => $data['email'] ?? null,
|
||||||
|
'password' => $data['password'],
|
||||||
|
'remember_token' => $data['remember_token'] ?? null,
|
||||||
|
'attributes' => json_encode($attributes)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data['id'] = $this->db->lastInsertId();
|
||||||
|
$data['attributes'] = $attributes;
|
||||||
|
|
||||||
|
// Fetch created/updated timestamps
|
||||||
|
$user = $this->findById($data['id']);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, array $data): bool
|
||||||
|
{
|
||||||
|
$attributes = array_merge($user->attributes, $data['attributes'] ?? []);
|
||||||
|
unset($data['attributes']);
|
||||||
|
|
||||||
|
$fields = [];
|
||||||
|
$params = ['id' => $user->id];
|
||||||
|
|
||||||
|
foreach (['username', 'email', 'password'] as $field) {
|
||||||
|
if (isset($data[$field])) {
|
||||||
|
$fields[] = "$field = :$field";
|
||||||
|
$params[$field] = $data[$field];
|
||||||
|
$user->$field = $data[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($attributes)) {
|
||||||
|
$fields[] = "attributes = :attributes";
|
||||||
|
$params['attributes'] = json_encode($attributes);
|
||||||
|
$user->attributes = $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fields)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "UPDATE {$this->table} SET " . implode(', ', $fields) . " WHERE id = :id";
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
|
||||||
|
return $stmt->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRememberToken(User $user, ?string $token): bool
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"UPDATE {$this->table} SET remember_token = :token WHERE id = :id"
|
||||||
|
);
|
||||||
|
|
||||||
|
return $stmt->execute([
|
||||||
|
'id' => $user->id,
|
||||||
|
'token' => $token
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyPassword(User $user, string $password): bool
|
||||||
|
{
|
||||||
|
return password_verify($password, $user->password);
|
||||||
|
}
|
||||||
|
}
|
||||||
190
auth/providers/SqliteUserProvider.php
Normal file
190
auth/providers/SqliteUserProvider.php
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../UserProviderInterface.php';
|
||||||
|
require_once __DIR__ . '/../User.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SqliteUserProvider stores users in SQLite database
|
||||||
|
*/
|
||||||
|
class SqliteUserProvider implements UserProviderInterface
|
||||||
|
{
|
||||||
|
private PDO $db;
|
||||||
|
private string $table;
|
||||||
|
|
||||||
|
public function __construct(string $databasePath, string $table = 'users')
|
||||||
|
{
|
||||||
|
$this->table = $table;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->db = new PDO("sqlite:$databasePath");
|
||||||
|
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$this->createTable();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
throw new Exception("Failed to connect to SQLite database: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create users table if it doesn't exist
|
||||||
|
*/
|
||||||
|
private function createTable(): void
|
||||||
|
{
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username VARCHAR(255) UNIQUE,
|
||||||
|
email VARCHAR(255) UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
remember_token VARCHAR(255),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
attributes TEXT
|
||||||
|
)";
|
||||||
|
|
||||||
|
$this->db->exec($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map database row to User object
|
||||||
|
*/
|
||||||
|
private function mapToUser(array $row): User
|
||||||
|
{
|
||||||
|
$attributes = isset($row['attributes']) ? json_decode($row['attributes'], true) ?? [] : [];
|
||||||
|
|
||||||
|
return new User([
|
||||||
|
'id' => $row['id'],
|
||||||
|
'username' => $row['username'],
|
||||||
|
'email' => $row['email'],
|
||||||
|
'password' => $row['password'],
|
||||||
|
'rememberToken' => $row['remember_token'],
|
||||||
|
'createdAt' => $row['created_at'],
|
||||||
|
'updatedAt' => $row['updated_at'],
|
||||||
|
'attributes' => $attributes
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findById(int|string $id): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByEmail(string $email): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE email = :email");
|
||||||
|
$stmt->execute(['email' => $email]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByUsername(string $username): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE username = :username");
|
||||||
|
$stmt->execute(['username' => $username]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByCredentials(string $identifier): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"SELECT * FROM {$this->table} WHERE email = :identifier OR username = :identifier"
|
||||||
|
);
|
||||||
|
$stmt->execute(['identifier' => $identifier]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByRememberToken(string $token): ?User
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE remember_token = :token");
|
||||||
|
$stmt->execute(['token' => $token]);
|
||||||
|
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return $row ? $this->mapToUser($row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): User
|
||||||
|
{
|
||||||
|
$attributes = $data['attributes'] ?? [];
|
||||||
|
unset($data['attributes']);
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"INSERT INTO {$this->table} (username, email, password, remember_token, attributes, created_at, updated_at)
|
||||||
|
VALUES (:username, :email, :password, :remember_token, :attributes, :created_at, :updated_at)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'username' => $data['username'] ?? null,
|
||||||
|
'email' => $data['email'] ?? null,
|
||||||
|
'password' => $data['password'],
|
||||||
|
'remember_token' => $data['remember_token'] ?? null,
|
||||||
|
'attributes' => json_encode($attributes),
|
||||||
|
'created_at' => $data['created_at'] ?? date('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => $data['updated_at'] ?? date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data['id'] = $this->db->lastInsertId();
|
||||||
|
$data['attributes'] = $attributes;
|
||||||
|
|
||||||
|
return new User($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, array $data): bool
|
||||||
|
{
|
||||||
|
$attributes = array_merge($user->attributes, $data['attributes'] ?? []);
|
||||||
|
unset($data['attributes']);
|
||||||
|
|
||||||
|
$fields = [];
|
||||||
|
$params = ['id' => $user->id];
|
||||||
|
|
||||||
|
foreach (['username', 'email', 'password'] as $field) {
|
||||||
|
if (isset($data[$field])) {
|
||||||
|
$fields[] = "$field = :$field";
|
||||||
|
$params[$field] = $data[$field];
|
||||||
|
$user->$field = $data[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($attributes)) {
|
||||||
|
$fields[] = "attributes = :attributes";
|
||||||
|
$params['attributes'] = json_encode($attributes);
|
||||||
|
$user->attributes = $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fields)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields[] = "updated_at = :updated_at";
|
||||||
|
$params['updated_at'] = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$sql = "UPDATE {$this->table} SET " . implode(', ', $fields) . " WHERE id = :id";
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
|
||||||
|
return $stmt->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRememberToken(User $user, ?string $token): bool
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"UPDATE {$this->table} SET remember_token = :token, updated_at = :updated_at WHERE id = :id"
|
||||||
|
);
|
||||||
|
|
||||||
|
return $stmt->execute([
|
||||||
|
'id' => $user->id,
|
||||||
|
'token' => $token,
|
||||||
|
'updated_at' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyPassword(User $user, string $password): bool
|
||||||
|
{
|
||||||
|
return password_verify($password, $user->password);
|
||||||
|
}
|
||||||
|
}
|
||||||
210
example-auth.php
Normal file
210
example-auth.php
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/web.php';
|
||||||
|
require_once __DIR__ . '/auth/Auth.php';
|
||||||
|
require_once __DIR__ . '/auth/AuthMiddleware.php';
|
||||||
|
require_once __DIR__ . '/auth/providers/JsonUserProvider.php';
|
||||||
|
require_once __DIR__ . '/auth/providers/SqliteUserProvider.php';
|
||||||
|
require_once __DIR__ . '/auth/providers/MysqlUserProvider.php';
|
||||||
|
|
||||||
|
// Choose your storage provider
|
||||||
|
// Option 1: JSON file storage
|
||||||
|
$userProvider = new JsonUserProvider(__DIR__ . '/storage/users.json');
|
||||||
|
|
||||||
|
// Option 2: SQLite database
|
||||||
|
// $userProvider = new SqliteUserProvider(__DIR__ . '/storage/database.sqlite');
|
||||||
|
|
||||||
|
// Option 3: MySQL database
|
||||||
|
// $userProvider = new MysqlUserProvider([
|
||||||
|
// 'host' => 'localhost',
|
||||||
|
// 'database' => 'myapp',
|
||||||
|
// 'username' => 'root',
|
||||||
|
// 'password' => 'password'
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// Create web application
|
||||||
|
$app = new Web(debug: true);
|
||||||
|
|
||||||
|
// Initialize auth system
|
||||||
|
$auth = new Auth($userProvider, new Session());
|
||||||
|
$authMiddleware = new AuthMiddleware($auth);
|
||||||
|
|
||||||
|
// Make auth available in all routes
|
||||||
|
$app->use(function($context, $next) use ($auth) {
|
||||||
|
$context->set('auth', $auth);
|
||||||
|
$next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
$app->get('/', function($context) {
|
||||||
|
$user = $context->get('auth')->user();
|
||||||
|
$context->html("<h1>Welcome " . ($user ? $user->username : 'Guest') . "</h1>");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registration page
|
||||||
|
$app->get('/register', function($context) {
|
||||||
|
$csrfToken = $context->session->token();
|
||||||
|
$context->html(<<<HTML
|
||||||
|
<form method="POST" action="/register">
|
||||||
|
<input type="hidden" name="_token" value="{$csrfToken}">
|
||||||
|
<input type="text" name="username" placeholder="Username" required><br>
|
||||||
|
<input type="email" name="email" placeholder="Email" required><br>
|
||||||
|
<input type="password" name="password" placeholder="Password" required><br>
|
||||||
|
<input type="password" name="password_confirmation" placeholder="Confirm Password" required><br>
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
HTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registration handler
|
||||||
|
$app->post('/register', function($context) use ($auth) {
|
||||||
|
// Validate input
|
||||||
|
$validator = $context->validate($context->request->all(), [
|
||||||
|
'username' => 'required|alphaNum|min:3|max:20',
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => 'required|min:6|confirmed'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if username/email already exists
|
||||||
|
if ($auth->provider->findByUsername($context->request->input('username'))) {
|
||||||
|
$context->error(400, 'Username already taken');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($auth->provider->findByEmail($context->request->input('email'))) {
|
||||||
|
$context->error(400, 'Email already registered');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register user
|
||||||
|
$user = $auth->register([
|
||||||
|
'username' => $context->request->input('username'),
|
||||||
|
'email' => $context->request->input('email'),
|
||||||
|
'password' => $context->request->input('password')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$context->redirect('/dashboard');
|
||||||
|
})->use($authMiddleware->verifyCsrf());
|
||||||
|
|
||||||
|
// Login page
|
||||||
|
$app->get('/login', function($context) {
|
||||||
|
$csrfToken = $context->session->token();
|
||||||
|
$context->html(<<<HTML
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<input type="hidden" name="_token" value="{$csrfToken}">
|
||||||
|
<input type="text" name="username" placeholder="Username or Email" required><br>
|
||||||
|
<input type="password" name="password" placeholder="Password" required><br>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="remember"> Remember me
|
||||||
|
</label><br>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
HTML);
|
||||||
|
})->use($authMiddleware->requireGuest());
|
||||||
|
|
||||||
|
// Login handler
|
||||||
|
$app->post('/login', function($context) use ($auth) {
|
||||||
|
$credentials = [
|
||||||
|
'username' => $context->request->input('username'),
|
||||||
|
'password' => $context->request->input('password')
|
||||||
|
];
|
||||||
|
|
||||||
|
$remember = $context->request->input('remember') === 'on';
|
||||||
|
|
||||||
|
if ($auth->attempt($credentials, $remember)) {
|
||||||
|
$context->redirect('/dashboard');
|
||||||
|
} else {
|
||||||
|
$context->error(401, 'Invalid credentials');
|
||||||
|
}
|
||||||
|
})->use($authMiddleware->verifyCsrf())
|
||||||
|
->use($authMiddleware->requireGuest());
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
$app->post('/logout', function($context) use ($auth) {
|
||||||
|
$auth->logout();
|
||||||
|
$context->redirect('/');
|
||||||
|
})->use($authMiddleware->verifyCsrf());
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
$app->group('/dashboard', function($app) use ($authMiddleware) {
|
||||||
|
// Apply auth middleware to all routes in this group
|
||||||
|
$app->use($authMiddleware->requireAuth());
|
||||||
|
|
||||||
|
$app->get('', function($context) {
|
||||||
|
$user = $context->get('user');
|
||||||
|
$csrfToken = $context->session->token();
|
||||||
|
$context->html(<<<HTML
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome, {$user->username}!</p>
|
||||||
|
<p>Email: {$user->email}</p>
|
||||||
|
<form method="POST" action="/logout" style="display:inline">
|
||||||
|
<input type="hidden" name="_token" value="{$csrfToken}">
|
||||||
|
<button type="submit">Logout</button>
|
||||||
|
</form>
|
||||||
|
HTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->get('/profile', function($context) {
|
||||||
|
$user = $context->get('user');
|
||||||
|
$context->json($user->toSafeArray());
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->post('/update-profile', function($context) use ($auth) {
|
||||||
|
$user = $context->get('user');
|
||||||
|
|
||||||
|
$validator = $context->validate($context->request->all(), [
|
||||||
|
'email' => 'email',
|
||||||
|
'username' => 'alphaNum|min:3|max:20'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $context->request->only(['email', 'username']);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
$auth->provider->update($user, $data);
|
||||||
|
$auth->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
$context->json(['success' => true, 'user' => $auth->user()->toSafeArray()]);
|
||||||
|
})->use($authMiddleware->verifyCsrf());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin only routes
|
||||||
|
$app->group('/admin', function($app) use ($authMiddleware) {
|
||||||
|
// Require admin role
|
||||||
|
$app->use($authMiddleware->requireRole('admin'));
|
||||||
|
|
||||||
|
$app->get('', function($context) {
|
||||||
|
$context->html('<h1>Admin Panel</h1>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API routes with rate limiting
|
||||||
|
$app->group('/api', function($app) use ($authMiddleware, $auth) {
|
||||||
|
// Optional auth for API
|
||||||
|
$app->use($authMiddleware->optional());
|
||||||
|
|
||||||
|
// Rate limiting: 60 requests per minute
|
||||||
|
$app->use($authMiddleware->rateLimit(60, 1));
|
||||||
|
|
||||||
|
$app->post('/login', function($context) use ($auth) {
|
||||||
|
$credentials = $context->request->only(['username', 'password']);
|
||||||
|
|
||||||
|
if ($auth->validate($credentials)) {
|
||||||
|
$auth->attempt($credentials);
|
||||||
|
return ['success' => true, 'user' => $auth->user()->toSafeArray()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context->json(['error' => 'Invalid credentials'], 401);
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->get('/me', function($context) use ($auth) {
|
||||||
|
if ($auth->guest()) {
|
||||||
|
return $context->json(['error' => 'Unauthenticated'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $auth->user()->toSafeArray();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
$app->run();
|
||||||
Loading…
x
Reference in New Issue
Block a user