create-prisma-php-app 5.0.0-alpha.8 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/.github/copilot-instructions.md +191 -0
- package/dist/AGENTS.md +588 -193
- package/dist/bootstrap.php +426 -213
- package/dist/index.js +2 -2
- package/dist/phpunit.xml +25 -0
- package/dist/postcss.config.js +4 -2
- package/dist/prisma-php.js +2 -2
- package/dist/public/.htaccess +1 -1
- package/dist/public/js/main.js +13 -1
- package/dist/public/js/pp-reactive-v2.min.js +1 -0
- package/dist/settings/auto-swagger-docs.ts +10 -10
- package/dist/settings/bs-config.ts +47 -27
- package/dist/settings/build.ts +2 -32
- package/dist/settings/component-map.ts +476 -0
- package/dist/settings/files-list.ts +2 -2
- package/dist/settings/project-name.ts +3 -6
- package/dist/settings/restart-mcp.ts +2 -2
- package/dist/settings/restart-websocket.ts +2 -2
- package/dist/settings/run-postcss.ts +205 -0
- package/dist/settings/run-tests.ts +35 -0
- package/dist/settings/swagger-config.ts +3 -3
- package/dist/settings/utils.ts +3 -3
- package/dist/src/Lib/Auth/Auth.php +62 -34
- package/dist/src/Lib/MCP/mcp-server.php +2 -3
- package/dist/src/Lib/Middleware/CorsMiddleware.php +72 -0
- package/dist/src/Lib/Websocket/ConnectionManager.php +460 -7
- package/dist/src/Lib/Websocket/Socket.php +170 -0
- package/dist/src/Lib/Websocket/SocketPool.php +50 -0
- package/dist/src/Lib/Websocket/SocketRegistry.php +88 -0
- package/dist/src/Lib/Websocket/sockets.php +50 -0
- package/dist/src/Lib/Websocket/websocket-server.php +10 -3
- package/dist/src/app/globals.css +3 -1
- package/dist/src/app/layout.php +1 -1
- package/dist/tests/AuthTest.php +59 -0
- package/dist/tests/ConnectionManagerTest.php +277 -0
- package/dist/tests/CsrfTest.php +119 -0
- package/dist/tests/DeferComponentRootsTest.php +147 -0
- package/dist/tests/FeaturesTest.php +41 -0
- package/dist/tests/README.md +119 -0
- package/dist/tests/RpcWireContractTest.php +101 -0
- package/dist/tests/SocketPoolTest.php +69 -0
- package/dist/tests/SocketRegistryTest.php +77 -0
- package/dist/tests/SocketTest.php +124 -0
- package/dist/tests/SocketsRegistrationTest.php +40 -0
- package/dist/tests/Support/FakeConnection.php +62 -0
- package/dist/tests/Support/Features.php +38 -0
- package/dist/tests/Support/RequiresFeature.php +26 -0
- package/dist/tests/bootstrap.php +44 -0
- package/dist/ts/main.ts +21 -3
- package/dist/ts/tailwind-merge.ts +13 -0
- package/package.json +4 -4
- package/dist/README.md +0 -213
- package/dist/public/js/pp-reactive-v2.js +0 -1
- package/dist/settings/bs-config.json +0 -6
- package/dist/settings/class-imports.ts +0 -165
- package/dist/settings/class-log.ts +0 -244
- package/dist/settings/component-import-checker.ts +0 -90
- package/dist/settings/files-list.json +0 -1
- package/dist/settings/prisma-schema-config.json +0 -16
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, readFileSync, rm, rmSync, writeFile } from "node:fs";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
const mode: "watch" | "build" = process.argv[2] === "watch" ? "watch" : "build";
|
|
8
|
+
const watcherPidFile = join(process.cwd(), ".pp", "postcss-watch.pid");
|
|
9
|
+
const mkdirAsync = promisify(mkdir);
|
|
10
|
+
const readFileAsync = promisify(readFile);
|
|
11
|
+
const rmAsync = promisify(rm);
|
|
12
|
+
const writeFileAsync = promisify(writeFile);
|
|
13
|
+
|
|
14
|
+
const args: string[] = [
|
|
15
|
+
"--max-old-space-size=6144",
|
|
16
|
+
"./node_modules/postcss-cli/index.js",
|
|
17
|
+
"src/app/globals.css",
|
|
18
|
+
"-o",
|
|
19
|
+
"public/css/styles.css",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (mode === "watch") {
|
|
23
|
+
args.push("--watch");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isValidPid(value: string): boolean {
|
|
27
|
+
return /^\d+$/.test(value.trim());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isProcessRunning(pid: number): boolean {
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function killWindowsProcessTree(pid: number): Promise<void> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const killer = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
|
|
42
|
+
resolve(),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
killer.on("error", () => resolve());
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function killProcessTree(pid: number): Promise<void> {
|
|
50
|
+
if (process.platform === "win32") {
|
|
51
|
+
await killWindowsProcessTree(pid);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, "SIGTERM");
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function ensurePidDirectory(): Promise<void> {
|
|
63
|
+
await mkdirAsync(dirname(watcherPidFile), { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function clearPidFile(): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const storedPid = (await readFileAsync(watcherPidFile, "utf8")).trim();
|
|
69
|
+
if (storedPid === String(process.pid)) {
|
|
70
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearPidFileSync(): void {
|
|
76
|
+
try {
|
|
77
|
+
const storedPid = readFileSync(watcherPidFile, "utf8").trim();
|
|
78
|
+
if (storedPid === String(process.pid)) {
|
|
79
|
+
rmSync(watcherPidFile, { force: true });
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function cleanupStaleWatcher(): Promise<void> {
|
|
85
|
+
if (mode !== "watch") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await ensurePidDirectory();
|
|
90
|
+
|
|
91
|
+
let storedPid = "";
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
storedPid = (await readFileAsync(watcherPidFile, "utf8")).trim();
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isValidPid(storedPid)) {
|
|
100
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const pid = Number(storedPid);
|
|
105
|
+
|
|
106
|
+
if (pid === process.pid) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isProcessRunning(pid)) {
|
|
111
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.warn(
|
|
116
|
+
`[tailwind] Found stale PostCSS watcher (PID ${pid}), stopping it before restart.`,
|
|
117
|
+
);
|
|
118
|
+
await killProcessTree(pid);
|
|
119
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function writePidFile(): Promise<void> {
|
|
123
|
+
if (mode !== "watch") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await ensurePidDirectory();
|
|
128
|
+
await writeFileAsync(watcherPidFile, `${process.pid}\n`, "utf8");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await cleanupStaleWatcher();
|
|
132
|
+
await writePidFile();
|
|
133
|
+
|
|
134
|
+
const child = spawn(process.execPath, args, {
|
|
135
|
+
stdio: "inherit",
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
env: {
|
|
138
|
+
...process.env,
|
|
139
|
+
PP_POSTCSS_MODE: mode,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let shuttingDown = false;
|
|
144
|
+
|
|
145
|
+
child.on("error", (error) => {
|
|
146
|
+
console.error(error);
|
|
147
|
+
void shutdown(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
async function shutdown(exitCode: number): Promise<void> {
|
|
151
|
+
if (shuttingDown) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
shuttingDown = true;
|
|
156
|
+
|
|
157
|
+
if (child.pid && child.exitCode === null && !child.killed) {
|
|
158
|
+
await killProcessTree(child.pid);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await clearPidFile();
|
|
162
|
+
process.exit(exitCode);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
child.on("exit", async (code, signal) => {
|
|
166
|
+
await clearPidFile();
|
|
167
|
+
|
|
168
|
+
if (shuttingDown) {
|
|
169
|
+
process.exit(code ?? 0);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (signal) {
|
|
174
|
+
try {
|
|
175
|
+
process.kill(process.pid, signal);
|
|
176
|
+
} catch {
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process.exit(code ?? 0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
process.once("SIGINT", () => {
|
|
186
|
+
void shutdown(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
process.once("SIGTERM", () => {
|
|
190
|
+
void shutdown(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
process.once("uncaughtException", (error) => {
|
|
194
|
+
console.error(error);
|
|
195
|
+
void shutdown(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
process.once("unhandledRejection", (reason) => {
|
|
199
|
+
console.error(reason);
|
|
200
|
+
void shutdown(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
process.once("exit", () => {
|
|
204
|
+
clearPidFileSync();
|
|
205
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import prismaPhpConfigJson from "../prisma-php.json";
|
|
5
|
+
import { getFileMeta } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
const { __dirname } = getFileMeta();
|
|
8
|
+
const projectRoot = join(__dirname, "..");
|
|
9
|
+
|
|
10
|
+
// The PHP binary the project is configured for (prisma-php.json), falling
|
|
11
|
+
// back to whatever `php` resolves to on PATH.
|
|
12
|
+
const phpExe =
|
|
13
|
+
prismaPhpConfigJson.phpRootPathExe && existsSync(prismaPhpConfigJson.phpRootPathExe)
|
|
14
|
+
? prismaPhpConfigJson.phpRootPathExe
|
|
15
|
+
: "php";
|
|
16
|
+
|
|
17
|
+
const phpunit = join(projectRoot, "vendor", "phpunit", "phpunit", "phpunit");
|
|
18
|
+
|
|
19
|
+
if (!existsSync(phpunit)) {
|
|
20
|
+
console.error(
|
|
21
|
+
"PHPUnit is not installed. Run: composer install (phpunit/phpunit is a dev dependency).",
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Everything after `npm run test --` is handed to PHPUnit, so
|
|
27
|
+
// `npm run test -- --filter CsrfTest` narrows the run.
|
|
28
|
+
const args = [phpunit, "--configuration", join(projectRoot, "phpunit.xml"), ...process.argv.slice(2)];
|
|
29
|
+
|
|
30
|
+
const result = spawnSync(phpExe, args, {
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
process.exit(result.status ?? 1);
|
|
@@ -11,7 +11,7 @@ const { __dirname } = getFileMeta();
|
|
|
11
11
|
export async function swaggerConfig(): Promise<void> {
|
|
12
12
|
const outputPath = join(
|
|
13
13
|
__dirname,
|
|
14
|
-
"../src/app/swagger-docs/apis/pp-swagger.json"
|
|
14
|
+
"../src/app/swagger-docs/apis/pp-swagger.json",
|
|
15
15
|
);
|
|
16
16
|
|
|
17
17
|
const options = {
|
|
@@ -56,8 +56,8 @@ export async function swaggerConfig(): Promise<void> {
|
|
|
56
56
|
writeFileSync(outputPath, swaggerSpec, "utf-8");
|
|
57
57
|
console.log(
|
|
58
58
|
`Swagger JSON has been generated and saved to ${chalk.blue(
|
|
59
|
-
"src/app/swagger-docs/apis/pp-swagger.json"
|
|
60
|
-
)}
|
|
59
|
+
"src/app/swagger-docs/apis/pp-swagger.json",
|
|
60
|
+
)}`,
|
|
61
61
|
);
|
|
62
62
|
} catch (error) {
|
|
63
63
|
console.error("Error saving Swagger JSON:", error);
|
package/dist/settings/utils.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function createSrcWatcher(
|
|
|
40
40
|
logPrefix?: string;
|
|
41
41
|
usePolling?: boolean;
|
|
42
42
|
interval?: number;
|
|
43
|
-
}
|
|
43
|
+
},
|
|
44
44
|
): FSWatcher {
|
|
45
45
|
const {
|
|
46
46
|
exts,
|
|
@@ -87,7 +87,7 @@ export class DebouncedWorker {
|
|
|
87
87
|
constructor(
|
|
88
88
|
private work: () => Promise<void> | void,
|
|
89
89
|
private debounceMs = 350,
|
|
90
|
-
private name = "worker"
|
|
90
|
+
private name = "worker",
|
|
91
91
|
) {}
|
|
92
92
|
|
|
93
93
|
schedule(reason?: string) {
|
|
@@ -172,7 +172,7 @@ export function createRestartableProcess(spec: {
|
|
|
172
172
|
function killOnWindows(pid: number): Promise<void> {
|
|
173
173
|
return new Promise((resolve) => {
|
|
174
174
|
const cp = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
|
|
175
|
-
resolve()
|
|
175
|
+
resolve(),
|
|
176
176
|
);
|
|
177
177
|
cp.on("error", () => resolve());
|
|
178
178
|
});
|
|
@@ -16,12 +16,14 @@ use Exception;
|
|
|
16
16
|
use InvalidArgumentException;
|
|
17
17
|
use ArrayObject;
|
|
18
18
|
use PP\Env;
|
|
19
|
+
use PP\Security\Csrf;
|
|
19
20
|
|
|
20
21
|
class Auth
|
|
21
22
|
{
|
|
22
23
|
public const PAYLOAD_NAME = 'payload_name_8639D';
|
|
23
24
|
public const ROLE_NAME = 'role';
|
|
24
25
|
public const PAYLOAD_SESSION_KEY = 'payload_session_key_2183A';
|
|
26
|
+
private const OAUTH_STATE_SESSION_KEY = 'oauth_state_61e4a';
|
|
25
27
|
|
|
26
28
|
public static string $cookieName = '';
|
|
27
29
|
|
|
@@ -32,7 +34,10 @@ class Auth
|
|
|
32
34
|
|
|
33
35
|
private function __construct()
|
|
34
36
|
{
|
|
35
|
-
$this->secretKey = Env::string('AUTH_SECRET', '
|
|
37
|
+
$this->secretKey = Env::string('AUTH_SECRET', '');
|
|
38
|
+
if ($this->secretKey === '') {
|
|
39
|
+
throw new InvalidArgumentException('AUTH_SECRET is required for authentication.');
|
|
40
|
+
}
|
|
36
41
|
self::$cookieName = self::getCookieName();
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -162,11 +167,15 @@ class Auth
|
|
|
162
167
|
/**
|
|
163
168
|
* Verifies the JWT token and returns the decoded payload if the token is valid.
|
|
164
169
|
* If the token is invalid or expired, null is returned.
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
170
|
+
*
|
|
171
|
+
* The payload is whatever `signIn(...)` stored: a scalar such as a role
|
|
172
|
+
* string, or an object for structured user data — so the return type is
|
|
173
|
+
* `mixed`, with `null` reserved for an invalid or expired token.
|
|
174
|
+
*
|
|
175
|
+
* @param string|null $jwt The JWT token to verify.
|
|
176
|
+
* @return mixed The decoded payload, or null if invalid or expired.
|
|
168
177
|
*/
|
|
169
|
-
public function verifyToken(?string $jwt):
|
|
178
|
+
public function verifyToken(?string $jwt): mixed
|
|
170
179
|
{
|
|
171
180
|
try {
|
|
172
181
|
if (!$jwt) return null;
|
|
@@ -256,27 +265,9 @@ class Auth
|
|
|
256
265
|
|
|
257
266
|
public function rotateCsrfToken(): void
|
|
258
267
|
{
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
$nonce = bin2hex(random_bytes(16));
|
|
266
|
-
$signature = hash_hmac('sha256', $nonce, $secret);
|
|
267
|
-
$token = $nonce . '.' . $signature;
|
|
268
|
-
|
|
269
|
-
if (!headers_sent()) {
|
|
270
|
-
setcookie('prisma_php_csrf', $token, [
|
|
271
|
-
'expires' => time() + 3600, // 1 hour validity
|
|
272
|
-
'path' => '/',
|
|
273
|
-
'secure' => $this->isHttpsRequest(),
|
|
274
|
-
'httponly' => false, // Must be FALSE so client JS can read it
|
|
275
|
-
'samesite' => 'Lax',
|
|
276
|
-
]);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
$_COOKIE['prisma_php_csrf'] = $token;
|
|
268
|
+
// The PulsePoint runtime reads the `pp_csrf` cookie family; issuing
|
|
269
|
+
// and naming live in one place so every writer stays aligned.
|
|
270
|
+
Csrf::rotate();
|
|
280
271
|
}
|
|
281
272
|
|
|
282
273
|
/**
|
|
@@ -386,26 +377,34 @@ class Auth
|
|
|
386
377
|
{
|
|
387
378
|
$dynamicRouteParams = Request::$dynamicParams[self::PPAUTH] ?? [];
|
|
388
379
|
|
|
389
|
-
if (Request::$isGet && in_array('signin', $dynamicRouteParams)) {
|
|
380
|
+
if (Request::$isGet && in_array('signin', $dynamicRouteParams, true)) {
|
|
390
381
|
foreach ($providers as $provider) {
|
|
391
|
-
if ($provider instanceof GithubProvider && in_array('github', $dynamicRouteParams)) {
|
|
392
|
-
$
|
|
382
|
+
if ($provider instanceof GithubProvider && in_array('github', $dynamicRouteParams, true)) {
|
|
383
|
+
$state = $this->createOAuthState('github');
|
|
384
|
+
$githubAuthUrl = "https://github.com/login/oauth/authorize?scope=user:email%20read:user&client_id={$provider->clientId}&state=" . urlencode($state);
|
|
393
385
|
Request::redirect($githubAuthUrl);
|
|
394
|
-
} elseif ($provider instanceof GoogleProvider && in_array('google', $dynamicRouteParams)) {
|
|
386
|
+
} elseif ($provider instanceof GoogleProvider && in_array('google', $dynamicRouteParams, true)) {
|
|
387
|
+
$state = $this->createOAuthState('google');
|
|
395
388
|
$googleAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth?"
|
|
396
389
|
. "scope=" . urlencode('email profile') . "&"
|
|
397
390
|
. "response_type=code&"
|
|
398
391
|
. "client_id=" . urlencode($provider->clientId) . "&"
|
|
399
|
-
. "redirect_uri=" . urlencode($provider->redirectUri)
|
|
392
|
+
. "redirect_uri=" . urlencode($provider->redirectUri) . "&"
|
|
393
|
+
. "state=" . urlencode($state);
|
|
400
394
|
Request::redirect($googleAuthUrl);
|
|
401
395
|
}
|
|
402
396
|
}
|
|
403
397
|
}
|
|
404
398
|
|
|
405
399
|
$authCode = Validator::string($_GET['code'] ?? '');
|
|
400
|
+
$authState = Validator::string($_GET['state'] ?? '', false);
|
|
401
|
+
|
|
402
|
+
if (Request::$isGet && in_array('callback', $dynamicRouteParams, true) && $authCode !== '') {
|
|
403
|
+
if (in_array('github', $dynamicRouteParams, true)) {
|
|
404
|
+
if (!$this->consumeOAuthState('github', $authState)) {
|
|
405
|
+
exit("Error occurred. Please try again.");
|
|
406
|
+
}
|
|
406
407
|
|
|
407
|
-
if (Request::$isGet && in_array('callback', $dynamicRouteParams) && isset($authCode)) {
|
|
408
|
-
if (in_array('github', $dynamicRouteParams)) {
|
|
409
408
|
$provider = $this->findProvider($providers, GithubProvider::class);
|
|
410
409
|
|
|
411
410
|
if (!$provider) {
|
|
@@ -413,7 +412,11 @@ class Auth
|
|
|
413
412
|
}
|
|
414
413
|
|
|
415
414
|
return $this->githubProvider($provider, $authCode);
|
|
416
|
-
} elseif (in_array('google', $dynamicRouteParams)) {
|
|
415
|
+
} elseif (in_array('google', $dynamicRouteParams, true)) {
|
|
416
|
+
if (!$this->consumeOAuthState('google', $authState)) {
|
|
417
|
+
exit("Error occurred. Please try again.");
|
|
418
|
+
}
|
|
419
|
+
|
|
417
420
|
$provider = $this->findProvider($providers, GoogleProvider::class);
|
|
418
421
|
|
|
419
422
|
if (!$provider) {
|
|
@@ -554,6 +557,31 @@ class Auth
|
|
|
554
557
|
}
|
|
555
558
|
}
|
|
556
559
|
|
|
560
|
+
private function createOAuthState(string $provider): string
|
|
561
|
+
{
|
|
562
|
+
$state = bin2hex(random_bytes(16));
|
|
563
|
+
$_SESSION[self::OAUTH_STATE_SESSION_KEY][$provider] = $state;
|
|
564
|
+
|
|
565
|
+
return $state;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private function consumeOAuthState(string $provider, string $state): bool
|
|
569
|
+
{
|
|
570
|
+
$expectedState = $_SESSION[self::OAUTH_STATE_SESSION_KEY][$provider] ?? '';
|
|
571
|
+
|
|
572
|
+
if (!is_string($expectedState) || $expectedState === '' || $state === '' || !hash_equals($expectedState, $state)) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
unset($_SESSION[self::OAUTH_STATE_SESSION_KEY][$provider]);
|
|
577
|
+
|
|
578
|
+
if (empty($_SESSION[self::OAUTH_STATE_SESSION_KEY])) {
|
|
579
|
+
unset($_SESSION[self::OAUTH_STATE_SESSION_KEY]);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
|
|
557
585
|
private static function getCookieName(): string
|
|
558
586
|
{
|
|
559
587
|
$authCookieName = Env::string('AUTH_COOKIE_NAME', 'auth_cookie_name_d36e5');
|
|
@@ -68,9 +68,8 @@ try {
|
|
|
68
68
|
$port,
|
|
69
69
|
$prefix,
|
|
70
70
|
null, // sslContext
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// , false // (optional) stateless
|
|
71
|
+
$enableJson, // enableJsonResponse
|
|
72
|
+
false // stateless
|
|
74
73
|
);
|
|
75
74
|
echo $color("✓ Listening on {$base}", '32') . PHP_EOL;
|
|
76
75
|
$server->listen($transport);
|
|
@@ -16,6 +16,11 @@ final class CorsMiddleware
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
$cfg = self::buildConfig($overrides);
|
|
19
|
+
$cfg = self::allowImplicitLoopbackOrigin($origin, $cfg);
|
|
20
|
+
|
|
21
|
+
if ($cfg['allowCredentials'] && self::listHasWildcard($cfg['allowedOrigins'])) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
if (!self::isAllowedOrigin($origin, $cfg['allowedOrigins'])) {
|
|
21
26
|
return;
|
|
@@ -82,6 +87,20 @@ final class CorsMiddleware
|
|
|
82
87
|
return $cfg;
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
private static function allowImplicitLoopbackOrigin(string $origin, array $cfg): array
|
|
91
|
+
{
|
|
92
|
+
if ($cfg['allowedOrigins'] !== []) {
|
|
93
|
+
return $cfg;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!self::isImplicitLoopbackOrigin($origin)) {
|
|
97
|
+
return $cfg;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$cfg['allowedOrigins'] = [self::normalize($origin)];
|
|
101
|
+
return $cfg;
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
private static function parseList(string $raw): array
|
|
86
105
|
{
|
|
87
106
|
$raw = trim($raw);
|
|
@@ -101,6 +120,59 @@ final class CorsMiddleware
|
|
|
101
120
|
return rtrim($origin, '/');
|
|
102
121
|
}
|
|
103
122
|
|
|
123
|
+
private static function isImplicitLoopbackOrigin(string $origin): bool
|
|
124
|
+
{
|
|
125
|
+
$originParts = parse_url(self::normalize($origin));
|
|
126
|
+
if (!is_array($originParts)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$originHost = strtolower((string) ($originParts['host'] ?? ''));
|
|
131
|
+
$originScheme = strtolower((string) ($originParts['scheme'] ?? ''));
|
|
132
|
+
$requestHost = self::getRequestHost();
|
|
133
|
+
$requestScheme = self::isHttpsRequest() ? 'https' : 'http';
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
$originHost === '' ||
|
|
137
|
+
$requestHost === '' ||
|
|
138
|
+
$originHost !== $requestHost ||
|
|
139
|
+
$originScheme !== $requestScheme
|
|
140
|
+
) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return self::isLoopbackHost($originHost);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static function getRequestHost(): string
|
|
148
|
+
{
|
|
149
|
+
$host = (string) ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '');
|
|
150
|
+
if ($host === '') {
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
$parts = parse_url('http://' . $host);
|
|
155
|
+
if (is_array($parts) && isset($parts['host'])) {
|
|
156
|
+
return strtolower((string) $parts['host']);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return strtolower(explode(':', $host, 2)[0]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static function isLoopbackHost(string $host): bool
|
|
163
|
+
{
|
|
164
|
+
return in_array(trim(strtolower($host), '[]'), ['localhost', '127.0.0.1', '::1'], true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private static function isHttpsRequest(): bool
|
|
168
|
+
{
|
|
169
|
+
return (
|
|
170
|
+
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
|
171
|
+
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
|
|
172
|
+
(isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
104
176
|
private static function isAllowedOrigin(string $origin, array $list): bool
|
|
105
177
|
{
|
|
106
178
|
$o = self::normalize($origin);
|