summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Weipert <git@mail.dweipert.de>2025-08-19 15:50:42 +0200
committerDaniel Weipert <git@mail.dweipert.de>2025-08-19 16:11:52 +0200
commitd08f4c83470c25d35d24594bd73e4effdac191a0 (patch)
tree8320e4d0750776891fa5680ce5904de714128fce
parenta0ad1f5e7fac279b33ea09ca0e347cd7d02cd8ec (diff)
database migrations and models for users, devices, tokens
-rwxr-xr-xbin/db-migrate36
-rw-r--r--docker-compose.yml7
-rw-r--r--migrations/20250819.php45
-rw-r--r--src/Controllers/LoginController.php73
-rw-r--r--src/DB.php22
-rw-r--r--src/Database.php36
-rw-r--r--src/Errors/AppException.php11
-rw-r--r--src/Models/Device.php150
-rw-r--r--src/Models/Tokens.php41
-rw-r--r--src/Models/User.php88
-rw-r--r--src/Router/Router.php2
-rw-r--r--src/Router/routes_client_server.php4
-rw-r--r--src/Support/ConnectsToDatabase.php24
-rw-r--r--src/Types/UserRegistrationKind.php9
14 files changed, 512 insertions, 36 deletions
diff --git a/bin/db-migrate b/bin/db-migrate
new file mode 100755
index 0000000..784aadc
--- /dev/null
+++ b/bin/db-migrate
@@ -0,0 +1,36 @@
+#!/bin/env php
+
+<?php
+
+use App\Database;
+use Symfony\Component\Dotenv\Dotenv;
+
+require_once dirname(__DIR__) . "/vendor/autoload.php";
+
+$dotenv = new Dotenv();
+$dotenv->load(dirname(__DIR__) . "/.env");
+
+$migrationsPath = dirname(__DIR__) . "/migrations";
+$migrations = scandir($migrationsPath, SCANDIR_SORT_ASCENDING);
+
+$appliedMigrations = [];
+try {
+ Database::getInstance()->query("select name from migrations")->fetchAll();
+} catch (\PDOException $exception) {
+ echo "migrations table doesn't exist yet.";
+}
+
+foreach ($migrations as $migration) {
+ if (in_array($migration, [".", ".."])) {
+ continue;
+ }
+
+ if (in_array($migration, $appliedMigrations)) {
+ continue;
+ }
+
+ $path = "$migrationsPath/$migration";
+ include $path;
+
+ Database::getInstance()->query("insert into migrations (name) values (:name)", ["name" => $migration]);
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 4ec2c2e..073a7c9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,6 +22,13 @@ services:
volumes:
- "./:/var/www/html"
+ adminer:
+ image: adminer
+ ports:
+ - "8081:8080"
+ environment:
+ - "ADMINER_DEFAULT_SERVER=db"
+
volumes:
db:
diff --git a/migrations/20250819.php b/migrations/20250819.php
new file mode 100644
index 0000000..cfd0f65
--- /dev/null
+++ b/migrations/20250819.php
@@ -0,0 +1,45 @@
+<?php
+
+use App\Database;
+
+Database::getInstance()->query(<<<SQL
+ create table if not exists "migrations" (
+ "id" bigserial primary key,
+ "name" varchar(64) not null
+ );
+SQL);
+
+Database::getInstance()->query(<<<SQL
+ create table if not exists "users" (
+ "id" varchar(255) primary key,
+ "password" varchar(255),
+
+ "name" varchar(255),
+ "is_deactivated" bool
+ );
+SQL);
+
+Database::getInstance()->query(<<<SQL
+ create table if not exists "devices" (
+ "id" varchar(255) not null,
+ "name" varchar(255) not null,
+
+ "user_id" varchar(255) references users(id) not null,
+
+ primary key (user_id, id)
+ );
+SQL);
+
+Database::getInstance()->query(<<<SQL
+ create table if not exists "tokens" (
+ "id" bigserial primary key,
+
+ "access_token" varchar(255) not null,
+ "refresh_token" varchar(255) not null,
+
+ "user_id" varchar(255) not null,
+ "device_id" varchar(255),
+
+ foreign key (user_id, device_id) references devices(user_id, id)
+ );
+SQL);
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";
+}