diff options
| author | Daniel Weipert <git@mail.dweipert.de> | 2025-08-19 15:50:42 +0200 | 
|---|---|---|
| committer | Daniel Weipert <git@mail.dweipert.de> | 2025-08-19 16:11:52 +0200 | 
| commit | d08f4c83470c25d35d24594bd73e4effdac191a0 (patch) | |
| tree | 8320e4d0750776891fa5680ce5904de714128fce /src | |
| parent | a0ad1f5e7fac279b33ea09ca0e347cd7d02cd8ec (diff) | |
database migrations and models for users, devices, tokens
Diffstat (limited to 'src')
| -rw-r--r-- | src/Controllers/LoginController.php | 73 | ||||
| -rw-r--r-- | src/DB.php | 22 | ||||
| -rw-r--r-- | src/Database.php | 36 | ||||
| -rw-r--r-- | src/Errors/AppException.php | 11 | ||||
| -rw-r--r-- | src/Models/Device.php | 150 | ||||
| -rw-r--r-- | src/Models/Tokens.php | 41 | ||||
| -rw-r--r-- | src/Models/User.php | 88 | ||||
| -rw-r--r-- | src/Router/Router.php | 2 | ||||
| -rw-r--r-- | src/Router/routes_client_server.php | 4 | ||||
| -rw-r--r-- | src/Support/ConnectsToDatabase.php | 24 | ||||
| -rw-r--r-- | src/Types/UserRegistrationKind.php | 9 | 
11 files changed, 424 insertions, 36 deletions
| diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index d48628b..1ff234c 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -2,10 +2,15 @@  namespace App\Controllers; +use App\Database; +use App\Errors\AppException; +use App\Errors\ErrorCode;  use App\Errors\UnknownError; -use App\Support\Parser; +use App\Models\Device; +use App\Models\User;  use App\Types\LoginFlow;  use App\Types\LoginType; +use App\Types\UserRegistrationKind;  use Symfony\Component\HttpFoundation\Request;  use Symfony\Component\HttpFoundation\Response;  use Symfony\Component\HttpFoundation\JsonResponse; @@ -14,7 +19,6 @@ class LoginController  {    /**     * GET /_matrix/client/r0/login -   * GET /_matrix/client/v3/login     */    public function supportedLoginTypes(): Response    { @@ -31,28 +35,77 @@ class LoginController    public function login(): Response    {      $request = Request::createFromGlobals(); -    $content = json_decode($request->getContent(), true); +    $body = json_decode($request->getContent(), true);      // validate login type      $loginType = null;      try { -      $loginType = LoginType::from($content["type"]); +      $loginType = LoginType::from($body["type"]);      } catch (\ValueError $error) {        throw new UnknownError("Bad login type.", Response::HTTP_BAD_REQUEST);      } -    // get user name -    $user = Parser::parseUser($content["identifier"]["user"]); +    // get user id +    $userId = $body["identifier"]["user"];      #if ($loginType == LoginType::PASSWORD) {} +    $user = User::fetchWithPassword($userId, $body["password"]); + +    if (! $user) { +      throw new AppException(ErrorCode::FORBIDDEN, "Invalid credentials.", Response::HTTP_FORBIDDEN); +    } + +    $deviceId = $body["device_id"] ?? ""; +    $device = $user->fetchDevice($deviceId); + +    if (! $device) { +      $device = Device::new( +        $user->getId(), +        initialDisplayName: $body["initial_device_display_name"] ?? "", +      ); +    } +      return new JsonResponse([ -      "access_token" => "abc123", -      "device_id" => "ABC", +      "access_token" => $device->getAccessToken(), +      "device_id" => $device->getId(),        "expires_in_ms" => 60000, -      "refresh_token" => "def456", -      "user_id" => "@{$user["username"]}:{$_ENV["DOMAIN"]}", +      "refresh_token" => $device->getRefreshToken(), +      "user_id" => $user->getId(),        #"well_known" => [],      ]);    } + +  /** +   * POST /_matrix/client/v3/register +   */ +  public function register(): Response +  { +    $request = Request::createFromGlobals(); +    $body = json_decode($request->getContent(), true); + +    $kind = UserRegistrationKind::from($request->query->get("kind") ?? "user"); + +    $username = $body["username"]; +    $userId = "@$username:$_ENV[DOMAIN]"; + +    Database::getInstance()->query("insert into users (id, password) values (:id, :password)", [ +      "id" => $userId, +      "password" => $body["password"], +    ]); + +    $device_id = $body["device_id"] ?? ""; +    $initialDeviceDisplayName = $body["initialDeviceDisplayName"] ?? ""; + +    $device = Device::new($userId, $device_id, $initialDeviceDisplayName); +    $device->insert(); + +    return new JsonResponse([ +      "access_token" => $device->getAccessToken(), +      "device_id" => $device->getId(), +      "expires_in_ms" => 60000, +      "refresh_token" => $device->getRefreshToken(), +      "user_id" => $userId, +    ]); +  }  } diff --git a/src/DB.php b/src/DB.php deleted file mode 100644 index 53d078b..0000000 --- a/src/DB.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php - -namespace App; - -class DB -{ -  use Singleton; - -  private \PDO $connection; - -  public function __construct() -  { -    $driver = $_ENV['DB_DRIVER'] ?? 'pgsql'; -    $host = $_ENV['DB_HOST'] ?? 'localhost'; -    $port = $_ENV['DB_PORT'] ?? 5432; -    $dbname = $_ENV['DB_NAME']; -    $user = $_ENV['DB_USER']; -    $password = $_ENV['DB_PASSWORD']; - -    $this->connection = new \PDO("$driver:host=$host;port=$port;dbname=$dbname", $user, $password); -  } -} diff --git a/src/Database.php b/src/Database.php new file mode 100644 index 0000000..3fff863 --- /dev/null +++ b/src/Database.php @@ -0,0 +1,36 @@ +<?php + +namespace App; + +use PDO; +use PDOStatement; + +class Database +{ +  use Singleton; + +  private \PDO $connection; + +  public function __construct() +  { +    #$driver = $_ENV['DB_DRIVER'] ?? 'pgsql'; +    $host = $_ENV['DB_HOST'] ?? 'localhost'; +    $port = $_ENV['DB_PORT'] ?? 5432; +    $dbname = $_ENV['DB_NAME']; +    $user = $_ENV['DB_USER']; +    $password = $_ENV['DB_PASSWORD']; + +    $this->connection = new \PDO("pgsql:host=$host;port=$port;dbname=$dbname", $user, $password); +  } + +  /** +   * @param array<mixed> $parameters +   */ +  public function query(string $query, array $parameters = []): \PDOStatement|false +  { +    $statement = $this->connection->prepare($query); +    $statement->execute($parameters); + +    return $statement; +  } +} diff --git a/src/Errors/AppException.php b/src/Errors/AppException.php new file mode 100644 index 0000000..e4359e6 --- /dev/null +++ b/src/Errors/AppException.php @@ -0,0 +1,11 @@ +<?php + +namespace App\Errors; + +class AppException extends Exception +{ +  public function getAdditionalData(): array +  { +    return []; +  } +} diff --git a/src/Models/Device.php b/src/Models/Device.php new file mode 100644 index 0000000..2b09c6b --- /dev/null +++ b/src/Models/Device.php @@ -0,0 +1,150 @@ +<?php + +namespace App\Models; + +use App\Database; +use App\Support\ConnectsToDatabase; + +class Device implements ConnectsToDatabase +{ +  public function __construct( +    private string $id, +    private string $userId, +    private string $name +  ) +  {} + +  /** +   * @param array<string,mixed> $row The row from the database +   */ +  public static function fromDatabase(array $row): self +  { +    return new self( +      $row["id"], +      $row["user_id"], +      $row["name"] +    ); +  } + +  public static function fetch(string $id = "", string $userId = ""): ?self +  { +    if (! empty($id) && empty($userId)) { +      throw new \InvalidArgumentException("Can't fetch device without user id."); +    } + +    $row = []; + +    if (! empty($userId)) { +      if (! empty($id)) { +        $row = Database::getInstance()->query( +          <<<SQL +            select * from devices where id=:id and user_id=:user_id +          SQL, +          [ +            "id" => $id, +            "user_id" => $userId, +          ] +        )->fetch(); +      } else { +        $row = Database::getInstance()->query( +          <<<SQL +            select * from devices where user_id=:user_id +          SQL, +          [ +            "user_id" => $userId, +          ] +        )->fetch(); +      } +    } + +    if (empty($row)) { +      return null; +    } + +    return self::fromDatabase($row); +  } + +  public static function fetchAll(string $userId = ""): array +  { +    if (empty($userId)) { +      throw new \InvalidArgumentException("missing user id"); +    } + +    $devices = []; +    $rows = Database::getInstance()->query( +      <<<SQL +        select * from devices +        where user_id=:user_id +      SQL, +      [ +        "user_id" => $userId, +      ] +    )->fetchAll(); + +    foreach ($rows as $row) { +      $devices[] = Device::fromDatabase($row); +    } + +    return $devices; +  } + +  public static function new(string $userId, string $deviceId = "", string $initialDisplayName = ""): self +  { +    $deviceId = $deviceId ?: md5($userId . random_bytes(512)); +    $initialDisplayName = $initialDisplayName ?: "capybara"; + +    $accessToken = md5($userId . random_bytes(512)); +    $refreshToken = md5($userId . random_bytes(512)); + +    return new self( +      $deviceId, +      $userId, +      $initialDisplayName, +      $accessToken, +      $refreshToken, +    ); +  } + +  public function insert(): bool +  { +    return Database::getInstance()->query( +      <<<SQL +        insert into devices (id, user_id, name, access_token, refresh_token) +        values (:id, :user_id, :name, :access_token, :refresh_token) +      SQL, +      [ +        "id" => $this->id, +        "user_id" => $this->userId, +        "name" => $this->name, +        "access_token" => $this->accessToken, +        "refresh_token" => $this->refreshToken, +      ] +    ); +  } + +  public function update(): bool +  {} + +  public function delete(): bool +  {} + +  public function getId(): string +  { +    return $this->id; +  } + +  public function getName(): string +  { +    return $this->name; +  } + +  public function getAccessToken(): string +  { +    return $this->accessToken; +  } + +  public function getRefreshToken(): string +  { +    return $this->refreshToken; +  } +} diff --git a/src/Models/Tokens.php b/src/Models/Tokens.php new file mode 100644 index 0000000..a94c876 --- /dev/null +++ b/src/Models/Tokens.php @@ -0,0 +1,41 @@ +<?php + +namespace App\Models; + +use App\Support\ConnectsToDatabase; + +class Tokens implements ConnectsToDatabase +{ +  public function __construct( +    private string $accessToken, +    private string $refreshToken, +    private string $userId, +    private string $deviceId = "", +  ) +  {} + +  public static function fromDatabase(array $row): self +  { +    return new self( +      $row["access_token"], +      $row["refresh_token"], +      $row["user_id"], +      $row["device_id"], +    ); +  } + +  public static function fetch(): ?self +  {} + +  public static function fetchAll(): array +  {} + +  public function insert(): bool +  {} + +  public function update(): bool +  {} + +  public function delete(): bool +  {} +} diff --git a/src/Models/User.php b/src/Models/User.php new file mode 100644 index 0000000..5d198e7 --- /dev/null +++ b/src/Models/User.php @@ -0,0 +1,88 @@ +<?php + +namespace App\Models; + +use App\Database; +use App\Support\ConnectsToDatabase; + +class User implements ConnectsToDatabase +{ +  public function __construct(private string $id) +  {} + +  public static function fromDatabase(array $row): self +  { +    return new self( +      $row["id"], +    ); +  } + +  public static function fetch(string $id = ""): ?self +  { +    if (empty($id)) { +      throw new \InvalidArgumentException("missing user id"); +    } + +    return Database::getInstance()->query( +      <<<SQL +        select * from users where id=:id +      SQL, +      [ +        "id" => $id, +      ] +    ); +  } + +  public static function fetchAll(): array +  {} + +  public static function fetchWithPassword(string $id, string $password): ?self +  { +    $row = Database::getInstance()->query("select * from users where id=:id and password=:password", [ +      "id" => $id, +      "password" => $password, +    ])->fetch(); + +    if (empty($row)) { +      return null; +    } + +    return self::fromDatabase($row); +  } + +  public function insert(): bool +  { +    return Database::getInstance()->query( +      <<<SQL +        insert into users (id) +        values (:id) +      SQL, +      [ +        "id" => $this->id, +      ] +    ); +  } + +  public function update(): bool +  {} + +  public function delete(): bool +  {} + +  public function getId(): string +  { +    return $this->id; +  } + +  public function fetchDevice(string $id): ?Device +  { +    return Device::fetch($id, $this->id); +  } +  /** +   * @return Device[] +   */ +  public function fetchDevices(): array +  { +    return Device::fetchAll($this->id); +  } +} diff --git a/src/Router/Router.php b/src/Router/Router.php index 1739e7c..61abed1 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -66,6 +66,8 @@ class Router        return new ErrorResponse(ErrorCode::NOT_FOUND, "404", Response::HTTP_NOT_FOUND);      } catch (MethodNotAllowedException $exception) {        return new ErrorResponse(ErrorCode::FORBIDDEN, "403", Response::HTTP_FORBIDDEN); +    } catch (\LogicException $exception) { // display logic exceptions normally +      throw $exception;      } catch (\Exception $exception) {        return new ErrorResponse(          ErrorCode::UNKNOWN, diff --git a/src/Router/routes_client_server.php b/src/Router/routes_client_server.php index 78e2e48..a0b117b 100644 --- a/src/Router/routes_client_server.php +++ b/src/Router/routes_client_server.php @@ -31,10 +31,6 @@ return function (RouteConfigurator $routes): void      ->add("matrix_client_r0_login_types", "/_matrix/client/r0/login")      ->controller($supportedLoginTypes)      ->methods(["GET"]); -  $routes -    ->add("matrix_client_v3_login_types", "/_matrix/client/v3/login") -    ->controller($supportedLoginTypes) -    ->methods(["GET"]);    $routes      ->add("matrix_client_v3_login", "/_matrix/client/v3/login") diff --git a/src/Support/ConnectsToDatabase.php b/src/Support/ConnectsToDatabase.php new file mode 100644 index 0000000..29f566c --- /dev/null +++ b/src/Support/ConnectsToDatabase.php @@ -0,0 +1,24 @@ +<?php + +namespace App\Support; + +interface ConnectsToDatabase +{ +  /** +   * @param array<string,mixed> $row +   */ +  public static function fromDatabase(array $row): self; + +  public static function fetch(): ?self; + +  /** +   * @return array<self> +   */ +  public static function fetchAll(): array; + +  public function insert(): bool; + +  public function update(): bool; + +  public function delete(): bool; +} diff --git a/src/Types/UserRegistrationKind.php b/src/Types/UserRegistrationKind.php new file mode 100644 index 0000000..23dee2f --- /dev/null +++ b/src/Types/UserRegistrationKind.php @@ -0,0 +1,9 @@ +<?php + +namespace App\Types; + +enum UserRegistrationKind: string +{ +  case USER = "user"; +  case GUEST = "guest"; +} | 
