init: React frontend + PHP/Slim backend chat app

This commit is contained in:
2026-05-14 10:46:20 +08:00
commit 893cd136c1
92 changed files with 20834 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
{
"name": "chat-app/server",
"description": "Chat App PHP Backend",
"type": "project",
"require": {
"php": ">=7.4",
"slim/slim": "^4.11",
"slim/psr7": "^1.6"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
+848
View File
@@ -0,0 +1,848 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "24a272e0ab0d1653c60704fcf3eb7e72",
"packages": [
{
"name": "fig/http-message-util",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message-util.git",
"reference": "9d94dc0154230ac39e5bf89398b324a86f63f765"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765",
"reference": "9d94dc0154230ac39e5bf89398b324a86f63f765",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^5.3 || ^7.0 || ^8.0"
},
"suggest": {
"psr/http-message": "The package containing the PSR-7 interfaces"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Fig\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-message-util/issues",
"source": "https://github.com/php-fig/http-message-util/tree/1.1.5"
},
"time": "2020-11-24T22:02:12+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"FastRoute\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"description": "Fast request router for PHP",
"keywords": [
"router",
"routing"
],
"support": {
"issues": "https://github.com/nikic/FastRoute/issues",
"source": "https://github.com/nikic/FastRoute/tree/master"
},
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "e616d01114759c4c489f93b099585439f795fe35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
"reference": "e616d01114759c4c489f93b099585439f795fe35",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory/tree/1.0.2"
},
"time": "2023-04-10T20:10:41+00:00"
},
{
"name": "psr/http-message",
"version": "1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/1.1"
},
"time": "2023-04-04T09:50:52+00:00"
},
{
"name": "psr/http-server-handler",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-handler.git",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side request handler",
"keywords": [
"handler",
"http",
"http-interop",
"psr",
"psr-15",
"psr-7",
"request",
"response",
"server"
],
"support": {
"source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
},
"time": "2023-04-10T20:06:20+00:00"
},
{
"name": "psr/http-server-middleware",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-middleware.git",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-handler": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side middleware",
"keywords": [
"http",
"http-interop",
"middleware",
"psr",
"psr-15",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-server-middleware/issues",
"source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
},
"time": "2023-04-11T06:14:47+00:00"
},
{
"name": "psr/log",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.1"
},
"time": "2024-08-21T13:31:24+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "slim/psr7",
"version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim-Psr7.git",
"reference": "5c64088c146673473f5667cbeb501edf5508dfaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/5c64088c146673473f5667cbeb501edf5508dfaf",
"reference": "5c64088c146673473f5667cbeb501edf5508dfaf",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"fig/http-message-util": "^1.1.5",
"php": "^7.4 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0",
"ralouphie/getallheaders": "^3.0",
"symfony/polyfill-php80": "^1.26"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"adriansuter/php-autoload-override": "^1.3",
"ext-json": "*",
"http-interop/http-factory-tests": "^1.0 || ^2.0",
"php-http/psr7-integration-tests": "^1.4",
"phpspec/prophecy": "^1.15",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^9.5 || ^10",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Slim\\Psr7\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Josh Lockhart",
"email": "hello@joshlockhart.com",
"homepage": "http://joshlockhart.com"
},
{
"name": "Andrew Smith",
"email": "a.smith@silentworks.co.uk",
"homepage": "http://silentworks.co.uk"
},
{
"name": "Rob Allen",
"email": "rob@akrabat.com",
"homepage": "http://akrabat.com"
},
{
"name": "Pierre Berube",
"email": "pierre@lgse.com",
"homepage": "http://www.lgse.com"
}
],
"description": "Strict PSR-7 implementation",
"homepage": "https://www.slimframework.com",
"keywords": [
"http",
"psr-7",
"psr7"
],
"support": {
"issues": "https://github.com/slimphp/Slim-Psr7/issues",
"source": "https://github.com/slimphp/Slim-Psr7/tree/1.6.2"
},
"time": "2025-03-23T11:43:29+00:00"
},
{
"name": "slim/slim",
"version": "4.13.0",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "038fd5713d5a41636fdff0e8dcceedecdd17fc17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/038fd5713d5a41636fdff0e8dcceedecdd17fc17",
"reference": "038fd5713d5a41636fdff0e8dcceedecdd17fc17",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"ext-json": "*",
"nikic/fast-route": "^1.3",
"php": "^7.4 || ^8.0",
"psr/container": "^1.0 || ^2.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"require-dev": {
"adriansuter/php-autoload-override": "^1.4",
"ext-simplexml": "*",
"guzzlehttp/psr7": "^2.6",
"httpsoft/http-message": "^1.1",
"httpsoft/http-server-request": "^1.1",
"laminas/laminas-diactoros": "^2.17 || ^3",
"nyholm/psr7": "^1.8",
"nyholm/psr7-server": "^1.1",
"phpspec/prophecy": "^1.19",
"phpspec/prophecy-phpunit": "^2.1",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.6",
"slim/http": "^1.3",
"slim/psr7": "^1.6",
"squizlabs/php_codesniffer": "^3.9"
},
"suggest": {
"ext-simplexml": "Needed to support XML format in BodyParsingMiddleware",
"ext-xml": "Needed to support XML format in BodyParsingMiddleware",
"php-di/php-di": "PHP-DI is the recommended container library to be used with Slim",
"slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information."
},
"type": "library",
"autoload": {
"psr-4": {
"Slim\\": "Slim"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Josh Lockhart",
"email": "hello@joshlockhart.com",
"homepage": "https://joshlockhart.com"
},
{
"name": "Andrew Smith",
"email": "a.smith@silentworks.co.uk",
"homepage": "http://silentworks.co.uk"
},
{
"name": "Rob Allen",
"email": "rob@akrabat.com",
"homepage": "http://akrabat.com"
},
{
"name": "Pierre Berube",
"email": "pierre@lgse.com",
"homepage": "http://www.lgse.com"
},
{
"name": "Gabriel Manricks",
"email": "gmanricks@me.com",
"homepage": "http://gabrielmanricks.com"
}
],
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
"homepage": "https://www.slimframework.com",
"keywords": [
"api",
"framework",
"micro",
"router"
],
"support": {
"docs": "https://www.slimframework.com/docs/v4/",
"forum": "https://discourse.slimframework.com/",
"irc": "irc://irc.freenode.net:6667/slimphp",
"issues": "https://github.com/slimphp/Slim/issues",
"rss": "https://www.slimframework.com/blog/feed.rss",
"slack": "https://slimphp.slack.com/",
"source": "https://github.com/slimphp/Slim",
"wiki": "https://github.com/slimphp/Slim/wiki"
},
"funding": [
{
"url": "https://opencollective.com/slimphp",
"type": "open_collective"
},
{
"url": "https://tidelift.com/funding/github/packagist/slim/slim",
"type": "tidelift"
}
],
"time": "2024-03-03T21:25:30+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-02T08:10:11+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.4"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}
+35
View File
@@ -0,0 +1,35 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Slim\Factory\AppFactory;
require __DIR__ . '/../src/config.php';
require __DIR__ . '/../src/db.php';
require __DIR__ . '/../src/jwt.php';
$app = AppFactory::create();
// CORS for dev
$app->add(function ($request, $handler) {
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
});
$app->addRoutingMiddleware();
$app->addErrorMiddleware(true, true, true);
// Options handler
$app->options('/{routes:.+}', function ($request, $response) {
return $response;
});
// Init DB
getDb();
// Register routes
require __DIR__ . '/../src/routes.php';
$app->run();
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'db_path' => __DIR__ . '/../chat.db',
'jwt_secret' => getenv('JWT_SECRET') ?: 'chat-terminal-secret-key-change-in-production',
'password' => getenv('CHAT_PASSWORD') ?: 'admin',
];
+58
View File
@@ -0,0 +1,58 @@
<?php
function getDb(): PDO {
static $pdo = null;
if ($pdo) return $pdo;
$config = require __DIR__ . '/config.php';
$dbPath = $config['db_path'];
$isNew = !file_exists($dbPath);
$pdo = new PDO('sqlite:' . $dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
if ($isNew) {
initDb($pdo);
}
return $pdo;
}
function initDb(PDO $pdo): void {
$pdo->exec("
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '新对话',
model_name TEXT,
model_base_url TEXT,
model_id TEXT,
api_key TEXT,
temperature REAL DEFAULT 0.7,
max_tokens INTEGER DEFAULT 4096,
system_prompt TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
content TEXT NOT NULL DEFAULT '',
timestamp INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id);
");
}
+51
View File
@@ -0,0 +1,51 @@
<?php
function jwtBase64UrlEncode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function jwtBase64UrlDecode(string $data): string {
$padding = 4 - strlen($data) % 4;
if ($padding !== 4) $data .= str_repeat('=', $padding);
return base64_decode(strtr($data, '-_', '+/'));
}
function jwtGenerate(string $userId): string {
$config = require __DIR__ . '/config.php';
$secret = $config['jwt_secret'];
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$time = time();
$payload = json_encode([
'userId' => $userId,
'iat' => $time,
'exp' => $time + 604800,
]);
$headerB64 = jwtBase64UrlEncode($header);
$payloadB64 = jwtBase64UrlEncode($payload);
$signature = hash_hmac('sha256', $headerB64 . '.' . $payloadB64, $secret, true);
$sigB64 = jwtBase64UrlEncode($signature);
return $headerB64 . '.' . $payloadB64 . '.' . $sigB64;
}
function jwtVerify(string $token): ?array {
$config = require __DIR__ . '/config.php';
$secret = $config['jwt_secret'];
$parts = explode('.', $token);
if (count($parts) !== 3) return null;
$payload = json_decode(jwtBase64UrlDecode($parts[1]), true);
if (!$payload || !isset($payload['exp']) || $payload['exp'] < time()) {
return null;
}
$signature = hash_hmac('sha256', $parts[0] . '.' . $parts[1], $secret, true);
$expected = jwtBase64UrlEncode($signature);
if (!hash_equals($expected, $parts[2])) return null;
return $payload;
}
+349
View File
@@ -0,0 +1,349 @@
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
// Auth middleware
$authMiddleware = function (Request $request, $handler) {
$authHeader = $request->getHeaderLine('Authorization');
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode(['error' => 'Unauthorized']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
$token = substr($authHeader, 7);
$decoded = jwtVerify($token);
if (!$decoded) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write(json_encode(['error' => 'Invalid token']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
$request = $request->withAttribute('userId', $decoded['userId']);
return $handler->handle($request);
};
// Health check
$app->get('/api/health', function (Request $request, Response $response) {
$response->getBody()->write(json_encode(['status' => 'ok']));
return $response->withHeader('Content-Type', 'application/json');
});
// Auth routes
$app->post('/api/auth/login', function (Request $request, Response $response) {
$body = json_decode((string)$request->getBody(), true);
$password = $body['password'] ?? '';
$config = require __DIR__ . '/config.php';
if (!$password) {
$response->getBody()->write(json_encode(['error' => 'Password required']));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
}
if ($password !== $config['password']) {
$response->getBody()->write(json_encode(['error' => 'Invalid password']));
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
}
$userId = 'admin';
$token = jwtGenerate($userId);
$response->getBody()->write(json_encode([
'token' => $token,
'user' => ['id' => $userId, 'username' => 'admin']
]));
return $response->withHeader('Content-Type', 'application/json');
});
$app->get('/api/auth/me', function (Request $request, Response $response) {
$userId = $request->getAttribute('userId');
$response->getBody()->write(json_encode([
'id' => $userId,
'username' => 'admin'
]));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
// Conversation routes
$app->get('/api/conversations', function (Request $request, Response $response) {
$userId = $request->getAttribute('userId');
$db = getDb();
$stmt = $db->prepare("SELECT id, title, model_name, model_base_url, model_id, temperature, max_tokens, created_at, updated_at FROM conversations WHERE user_id = ? ORDER BY updated_at DESC");
$stmt->execute([$userId]);
$rows = $stmt->fetchAll();
$response->getBody()->write(json_encode($rows));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
$app->get('/api/conversations/{id}', function (Request $request, Response $response, array $args) {
$userId = $request->getAttribute('userId');
$id = $args['id'];
$db = getDb();
$stmt = $db->prepare('SELECT * FROM conversations WHERE id = ? AND user_id = ?');
$stmt->execute([$id, $userId]);
$conv = $stmt->fetch();
if (!$conv) {
$response->getBody()->write(json_encode(['error' => 'Conversation not found']));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$stmt = $db->prepare('SELECT id, role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC');
$stmt->execute([$id]);
$messages = $stmt->fetchAll();
$response->getBody()->write(json_encode(array_merge($conv, ['messages' => $messages])));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
$app->post('/api/conversations', function (Request $request, Response $response) {
$userId = $request->getAttribute('userId');
$body = json_decode((string)$request->getBody(), true);
$title = $body['title'] ?? '新对话';
$modelConfig = $body['modelConfig'] ?? null;
$db = getDb();
$id = bin2hex(random_bytes(16));
$now = time() * 1000;
$stmt = $db->prepare('INSERT INTO conversations (id, user_id, title, model_name, model_base_url, model_id, api_key, temperature, max_tokens, system_prompt, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
$stmt->execute([
$id, $userId, $title,
$modelConfig['name'] ?? null,
$modelConfig['baseUrl'] ?? null,
$modelConfig['model'] ?? null,
$modelConfig['apiKey'] ?? null,
$modelConfig['temperature'] ?? 0.7,
$modelConfig['maxTokens'] ?? 4096,
$modelConfig['systemPrompt'] ?? null,
$now, $now
]);
$stmt = $db->prepare('SELECT * FROM conversations WHERE id = ?');
$stmt->execute([$id]);
$conv = $stmt->fetch();
$response->getBody()->write(json_encode(array_merge($conv, ['messages' => []])));
return $response->withStatus(201)->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
$app->put('/api/conversations/{id}', function (Request $request, Response $response, array $args) {
$userId = $request->getAttribute('userId');
$id = $args['id'];
$body = json_decode((string)$request->getBody(), true);
$title = $body['title'] ?? null;
$modelConfig = $body['modelConfig'] ?? null;
$db = getDb();
$stmt = $db->prepare('SELECT id FROM conversations WHERE id = ? AND user_id = ?');
$stmt->execute([$id, $userId]);
if (!$stmt->fetch()) {
$response->getBody()->write(json_encode(['error' => 'Conversation not found']));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$now = time() * 1000;
$stmt = $db->prepare('UPDATE conversations SET title = COALESCE(?, title), model_name = COALESCE(?, model_name), model_base_url = COALESCE(?, model_base_url), model_id = COALESCE(?, model_id), api_key = COALESCE(?, api_key), temperature = COALESCE(?, temperature), max_tokens = COALESCE(?, max_tokens), system_prompt = COALESCE(?, system_prompt), updated_at = ? WHERE id = ?');
$stmt->execute([
$title,
$modelConfig['name'] ?? null,
$modelConfig['baseUrl'] ?? null,
$modelConfig['model'] ?? null,
$modelConfig['apiKey'] ?? null,
$modelConfig['temperature'] ?? null,
$modelConfig['maxTokens'] ?? null,
$modelConfig['systemPrompt'] ?? null,
$now, $id
]);
$stmt = $db->prepare('SELECT * FROM conversations WHERE id = ?');
$stmt->execute([$id]);
$conv = $stmt->fetch();
$stmt = $db->prepare('SELECT id, role, content, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC');
$stmt->execute([$id]);
$messages = $stmt->fetchAll();
$response->getBody()->write(json_encode(array_merge($conv, ['messages' => $messages])));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
$app->delete('/api/conversations/{id}', function (Request $request, Response $response, array $args) {
$userId = $request->getAttribute('userId');
$id = $args['id'];
$db = getDb();
$stmt = $db->prepare('DELETE FROM conversations WHERE id = ? AND user_id = ?');
$stmt->execute([$id, $userId]);
if ($stmt->rowCount() === 0) {
$response->getBody()->write(json_encode(['error' => 'Conversation not found']));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$response->getBody()->write(json_encode(['success' => true]));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
$app->delete('/api/conversations/{id}/messages', function (Request $request, Response $response, array $args) {
$userId = $request->getAttribute('userId');
$id = $args['id'];
$db = getDb();
$stmt = $db->prepare('SELECT id FROM conversations WHERE id = ? AND user_id = ?');
$stmt->execute([$id, $userId]);
if (!$stmt->fetch()) {
$response->getBody()->write(json_encode(['error' => 'Conversation not found']));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$stmt = $db->prepare('DELETE FROM messages WHERE conversation_id = ?');
$stmt->execute([$id]);
$now = time() * 1000;
$stmt = $db->prepare('UPDATE conversations SET updated_at = ? WHERE id = ?');
$stmt->execute([$now, $id]);
$response->getBody()->write(json_encode(['success' => true]));
return $response->withHeader('Content-Type', 'application/json');
})->add($authMiddleware);
// Chat route (SSE)
$app->post('/api/chat', function (Request $request, Response $response) {
$userId = $request->getAttribute('userId');
$body = json_decode((string)$request->getBody(), true);
$conversationId = $body['conversationId'] ?? '';
$content = trim($body['content'] ?? '');
if (!$conversationId || !$content) {
$response->getBody()->write(json_encode(['error' => 'conversationId and content required']));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
}
$db = getDb();
$stmt = $db->prepare('SELECT * FROM conversations WHERE id = ? AND user_id = ?');
$stmt->execute([$conversationId, $userId]);
$conv = $stmt->fetch();
if (!$conv) {
$response->getBody()->write(json_encode(['error' => 'Conversation not found']));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
// Load history
$stmt = $db->prepare("SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC");
$stmt->execute([$conversationId]);
$history = $stmt->fetchAll();
// Build messages
$messages = [];
if ($conv['system_prompt']) {
$messages[] = ['role' => 'system', 'content' => $conv['system_prompt']];
}
foreach ($history as $msg) {
if ($msg['role'] !== 'system') {
$messages[] = ['role' => $msg['role'], 'content' => $msg['content']];
}
}
$messages[] = ['role' => 'user', 'content' => $content];
// Save user message
$userMsgId = bin2hex(random_bytes(16));
$now = time() * 1000;
$stmt = $db->prepare('INSERT INTO messages (id, conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$userMsgId, $conversationId, 'user', $content, $now]);
// Update title if first message
$stmt = $db->prepare('SELECT COUNT(*) as cnt FROM messages WHERE conversation_id = ?');
$stmt->execute([$conversationId]);
$msgCount = $stmt->fetch()['cnt'] ?? 0;
if ($msgCount == 1) {
$newTitle = mb_substr($content, 0, 20) . (mb_strlen($content) > 20 ? '...' : '');
$stmt = $db->prepare('UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?');
$stmt->execute([$newTitle, $now, $conversationId]);
}
// Check model config
if (!$conv['model_base_url'] || !$conv['api_key'] || !$conv['model_id']) {
$errMsg = 'Error: 模型配置不完整,请先在「配置」中设置 API 地址、API Key 和模型 ID';
$errMsgId = bin2hex(random_bytes(16));
$stmt = $db->prepare('INSERT INTO messages (id, conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$errMsgId, $conversationId, 'assistant', $errMsg, $now]);
$response->getBody()->write(json_encode(['error' => 'Model config incomplete']));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
}
// SSE output
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
while (ob_get_level() > 0) ob_end_flush();
ob_implicit_flush(true);
// Send user message confirmation
echo "data: " . json_encode(['type' => 'user', 'id' => $userMsgId, 'content' => $content]) . "\n\n";
flush();
// Call LLM API via curl
$assistantMsgId = bin2hex(random_bytes(16));
$fullContent = '';
$payload = json_encode([
'model' => $conv['model_id'],
'messages' => $messages,
'temperature' => (float)$conv['temperature'],
'max_tokens' => (int)$conv['max_tokens'],
'stream' => true,
]);
$ch = curl_init($conv['model_base_url'] . '/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $conv['api_key'],
],
CURLOPT_RETURNTRANSFER => false,
CURLOPT_WRITEFUNCTION => function ($ch, $data) use (&$fullContent) {
$lines = explode("\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if (!$line || !str_starts_with($line, 'data: ')) continue;
$jsonStr = substr($line, 6);
if ($jsonStr === '[DONE]') continue;
$parsed = json_decode($jsonStr, true);
if (!$parsed) continue;
$delta = $parsed['choices'][0]['delta']['content'] ?? '';
if ($delta) {
$fullContent .= $delta;
echo "data: " . json_encode(['type' => 'delta', 'content' => $delta]) . "\n\n";
flush();
}
}
return strlen($data);
},
CURLOPT_TIMEOUT => 120,
CURLOPT_CONNECTTIMEOUT => 30,
]);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
$errMsg = 'Error: ' . $error;
echo "data: " . json_encode(['type' => 'error', 'content' => $errMsg]) . "\n\n";
}
// Save assistant message
$stmt = $db->prepare('INSERT INTO messages (id, conversation_id, role, content, timestamp) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$assistantMsgId, $conversationId, 'assistant', $fullContent, time() * 1000]);
$stmt = $db->prepare('UPDATE conversations SET updated_at = ? WHERE id = ?');
$stmt->execute([time() * 1000, $conversationId]);
echo "data: " . json_encode(['type' => 'done', 'id' => $assistantMsgId, 'content' => $fullContent]) . "\n\n";
echo "data: [DONE]\n\n";
flush();
exit(0);
})->add($authMiddleware);