feat: OIDC (#564)
* feat: oidc feat: organization switcher refactor: org context feat: invitations GLM * feat: link current account * refactor: own page for sso registration * feat: per-user account management * refactor: code style * refactor: user existing check * refactor: restrict provider configuration to super admins only * refactor: cleanup / pr review * chore: fix lint issues * chore: pr feedbacks * test(e2e): automated tests for OIDC * fix: check url first for sso provider identification * fix: prevent oidc provider to be named "credential"
This commit is contained in:
parent
b2d2f28b40
commit
7a3932f969
79 changed files with 12915 additions and 7875 deletions
10
.github/workflows/e2e.yml
vendored
10
.github/workflows/e2e.yml
vendored
|
|
@ -51,6 +51,7 @@ jobs:
|
||||||
echo "SERVER_IP=localhost" >> .env.local
|
echo "SERVER_IP=localhost" >> .env.local
|
||||||
echo "ZEROBYTE_DATABASE_URL=./data/zerobyte.db" >> .env.local
|
echo "ZEROBYTE_DATABASE_URL=./data/zerobyte.db" >> .env.local
|
||||||
echo "BASE_URL=http://localhost:4096" >> .env.local
|
echo "BASE_URL=http://localhost:4096" >> .env.local
|
||||||
|
echo "E2E_DEX_ORIGIN=http://dex:5557" >> .env.local
|
||||||
|
|
||||||
- name: Start zerobyte-e2e service
|
- name: Start zerobyte-e2e service
|
||||||
run: bun run start:e2e -- -d
|
run: bun run start:e2e -- -d
|
||||||
|
|
@ -59,9 +60,13 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
timeout 30s bash -c 'until curl -f http://localhost:4096/api/healthcheck; do echo "Waiting for server..." && sleep 2; done'
|
timeout 30s bash -c 'until curl -f http://localhost:4096/api/healthcheck; do echo "Waiting for server..." && sleep 2; done'
|
||||||
|
|
||||||
|
- name: Wait for Dex to be ready
|
||||||
|
run: |
|
||||||
|
timeout 30s bash -c 'until curl -sf http://localhost:5557/dex/.well-known/openid-configuration; do echo "Waiting for Dex..." && sleep 2; done'
|
||||||
|
|
||||||
- name: Print docker logs if failed to start
|
- name: Print docker logs if failed to start
|
||||||
if: failure()
|
if: failure()
|
||||||
run: docker logs zerobyte || true
|
run: docker compose logs zerobyte-e2e dex || true
|
||||||
|
|
||||||
- name: Make playwright directory writable
|
- name: Make playwright directory writable
|
||||||
run: sudo chmod 777 playwright/data/zerobyte.db
|
run: sudo chmod 777 playwright/data/zerobyte.db
|
||||||
|
|
@ -73,7 +78,8 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
tree playwright
|
tree playwright
|
||||||
docker logs zerobyte > playwright-report/container-logs.txt || true
|
docker compose logs zerobyte-e2e > playwright-report/container-logs.txt || true
|
||||||
|
docker compose logs dex > playwright-report/dex-logs.txt || true
|
||||||
|
|
||||||
- name: Debug - print content of /test-data in container
|
- name: Debug - print content of /test-data in container
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -28,6 +28,7 @@ node_modules/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/playwright/.auth/
|
/playwright/.auth/
|
||||||
|
/playwright/restic.pass
|
||||||
|
|
||||||
playwright/.auth
|
playwright/.auth
|
||||||
playwright/temp
|
playwright/temp
|
||||||
|
|
@ -39,3 +40,4 @@ openapi-ts-error-*.log
|
||||||
.output
|
.output
|
||||||
tmp/
|
tmp/
|
||||||
qa-output
|
qa-output
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@
|
||||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf",
|
||||||
|
"ignorePatterns": ["*.gen.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Important instructions
|
## Important instructions
|
||||||
|
|
||||||
- Never create migration files manually. Always use the provided command to generate migrations
|
- Never create migration files manually. Always use the provided command to generate migrations
|
||||||
|
|
@ -62,3 +60,8 @@ bunx oxfmt format --write <path>
|
||||||
# Lint
|
# Lint
|
||||||
bun run lint
|
bun run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Invalidation
|
||||||
|
|
||||||
|
The frontend has an automatic invalidation setup which runs after every mutation.
|
||||||
|
Do not implement any invalidation logic in the frontend.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,286 +1,289 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { createSseClient } from "../core/serverSentEvents.gen";
|
import { createSseClient } from '../core/serverSentEvents.gen';
|
||||||
import type { HttpMethod } from "../core/types.gen";
|
import type { HttpMethod } from '../core/types.gen';
|
||||||
import { getValidRequestBody } from "../core/utils.gen";
|
import { getValidRequestBody } from '../core/utils.gen';
|
||||||
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen";
|
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
|
||||||
import {
|
import {
|
||||||
buildUrl,
|
buildUrl,
|
||||||
createConfig,
|
createConfig,
|
||||||
createInterceptors,
|
createInterceptors,
|
||||||
getParseAs,
|
getParseAs,
|
||||||
mergeConfigs,
|
mergeConfigs,
|
||||||
mergeHeaders,
|
mergeHeaders,
|
||||||
setAuthParams,
|
setAuthParams,
|
||||||
} from "./utils.gen";
|
} from './utils.gen';
|
||||||
|
|
||||||
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
|
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
|
||||||
body?: any;
|
body?: any;
|
||||||
headers: ReturnType<typeof mergeHeaders>;
|
headers: ReturnType<typeof mergeHeaders>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createClient = (config: Config = {}): Client => {
|
export const createClient = (config: Config = {}): Client => {
|
||||||
let _config = mergeConfigs(createConfig(), config);
|
let _config = mergeConfigs(createConfig(), config);
|
||||||
|
|
||||||
const getConfig = (): Config => ({ ..._config });
|
const getConfig = (): Config => ({ ..._config });
|
||||||
|
|
||||||
const setConfig = (config: Config): Config => {
|
const setConfig = (config: Config): Config => {
|
||||||
_config = mergeConfigs(_config, config);
|
_config = mergeConfigs(_config, config);
|
||||||
return getConfig();
|
return getConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
|
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
|
||||||
|
|
||||||
const beforeRequest = async (options: RequestOptions) => {
|
const beforeRequest = async (options: RequestOptions) => {
|
||||||
const opts = {
|
const opts = {
|
||||||
..._config,
|
..._config,
|
||||||
...options,
|
...options,
|
||||||
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
||||||
headers: mergeHeaders(_config.headers, options.headers),
|
headers: mergeHeaders(_config.headers, options.headers),
|
||||||
serializedBody: undefined,
|
serializedBody: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.security) {
|
if (opts.security) {
|
||||||
await setAuthParams({
|
await setAuthParams({
|
||||||
...opts,
|
...opts,
|
||||||
security: opts.security,
|
security: opts.security,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.requestValidator) {
|
if (opts.requestValidator) {
|
||||||
await opts.requestValidator(opts);
|
await opts.requestValidator(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.body !== undefined && opts.bodySerializer) {
|
if (opts.body !== undefined && opts.bodySerializer) {
|
||||||
opts.serializedBody = opts.bodySerializer(opts.body);
|
opts.serializedBody = opts.bodySerializer(opts.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove Content-Type header if body is empty to avoid sending invalid requests
|
// remove Content-Type header if body is empty to avoid sending invalid requests
|
||||||
if (opts.body === undefined || opts.serializedBody === "") {
|
if (opts.body === undefined || opts.serializedBody === '') {
|
||||||
opts.headers.delete("Content-Type");
|
opts.headers.delete('Content-Type');
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = buildUrl(opts);
|
const url = buildUrl(opts);
|
||||||
|
|
||||||
return { opts, url };
|
return { opts, url };
|
||||||
};
|
};
|
||||||
|
|
||||||
const request: Client["request"] = async (options) => {
|
const request: Client['request'] = async (options) => {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const { opts, url } = await beforeRequest(options);
|
const { opts, url } = await beforeRequest(options);
|
||||||
const requestInit: ReqInit = {
|
const requestInit: ReqInit = {
|
||||||
redirect: "follow",
|
redirect: 'follow',
|
||||||
...opts,
|
...opts,
|
||||||
body: getValidRequestBody(opts),
|
body: getValidRequestBody(opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = new Request(url, requestInit);
|
let request = new Request(url, requestInit);
|
||||||
|
|
||||||
for (const fn of interceptors.request.fns) {
|
for (const fn of interceptors.request.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
request = await fn(request, opts);
|
request = await fn(request, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
// fetch must be assigned here, otherwise it would throw the error:
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
const _fetch = opts.fetch!;
|
const _fetch = opts.fetch!;
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await _fetch(request);
|
response = await _fetch(request);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle fetch exceptions (AbortError, network errors, etc.)
|
// Handle fetch exceptions (AbortError, network errors, etc.)
|
||||||
let finalError = error;
|
let finalError = error;
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
for (const fn of interceptors.error.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
|
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalError = finalError || ({} as unknown);
|
finalError = finalError || ({} as unknown);
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
if (opts.throwOnError) {
|
||||||
throw finalError;
|
throw finalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return error response
|
// Return error response
|
||||||
return opts.responseStyle === "data"
|
return opts.responseStyle === 'data'
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
error: finalError,
|
error: finalError,
|
||||||
request,
|
request,
|
||||||
response: undefined as any,
|
response: undefined as any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const fn of interceptors.response.fns) {
|
for (const fn of interceptors.response.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
response = await fn(response, request, opts);
|
response = await fn(response, request, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const parseAs =
|
const parseAs =
|
||||||
(opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
|
(opts.parseAs === 'auto'
|
||||||
|
? getParseAs(response.headers.get('Content-Type'))
|
||||||
|
: opts.parseAs) ?? 'json';
|
||||||
|
|
||||||
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
|
||||||
let emptyData: any;
|
let emptyData: any;
|
||||||
switch (parseAs) {
|
switch (parseAs) {
|
||||||
case "arrayBuffer":
|
case 'arrayBuffer':
|
||||||
case "blob":
|
case 'blob':
|
||||||
case "text":
|
case 'text':
|
||||||
emptyData = await response[parseAs]();
|
emptyData = await response[parseAs]();
|
||||||
break;
|
break;
|
||||||
case "formData":
|
case 'formData':
|
||||||
emptyData = new FormData();
|
emptyData = new FormData();
|
||||||
break;
|
break;
|
||||||
case "stream":
|
case 'stream':
|
||||||
emptyData = response.body;
|
emptyData = response.body;
|
||||||
break;
|
break;
|
||||||
case "json":
|
case 'json':
|
||||||
default:
|
default:
|
||||||
emptyData = {};
|
emptyData = {};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return opts.responseStyle === "data"
|
return opts.responseStyle === 'data'
|
||||||
? emptyData
|
? emptyData
|
||||||
: {
|
: {
|
||||||
data: emptyData,
|
data: emptyData,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: any;
|
let data: any;
|
||||||
switch (parseAs) {
|
switch (parseAs) {
|
||||||
case "arrayBuffer":
|
case 'arrayBuffer':
|
||||||
case "blob":
|
case 'blob':
|
||||||
case "formData":
|
case 'formData':
|
||||||
case "text":
|
case 'text':
|
||||||
data = await response[parseAs]();
|
data = await response[parseAs]();
|
||||||
break;
|
break;
|
||||||
case "json": {
|
case 'json': {
|
||||||
// Some servers return 200 with no Content-Length and empty body.
|
// Some servers return 200 with no Content-Length and empty body.
|
||||||
// response.json() would throw; read as text and parse if non-empty.
|
// response.json() would throw; read as text and parse if non-empty.
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
data = text ? JSON.parse(text) : {};
|
data = text ? JSON.parse(text) : {};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "stream":
|
case 'stream':
|
||||||
return opts.responseStyle === "data"
|
return opts.responseStyle === 'data'
|
||||||
? response.body
|
? response.body
|
||||||
: {
|
: {
|
||||||
data: response.body,
|
data: response.body,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseAs === "json") {
|
if (parseAs === 'json') {
|
||||||
if (opts.responseValidator) {
|
if (opts.responseValidator) {
|
||||||
await opts.responseValidator(data);
|
await opts.responseValidator(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.responseTransformer) {
|
if (opts.responseTransformer) {
|
||||||
data = await opts.responseTransformer(data);
|
data = await opts.responseTransformer(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts.responseStyle === "data"
|
return opts.responseStyle === 'data'
|
||||||
? data
|
? data
|
||||||
: {
|
: {
|
||||||
data,
|
data,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const textError = await response.text();
|
const textError = await response.text();
|
||||||
let jsonError: unknown;
|
let jsonError: unknown;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jsonError = JSON.parse(textError);
|
jsonError = JSON.parse(textError);
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = jsonError ?? textError;
|
const error = jsonError ?? textError;
|
||||||
let finalError = error;
|
let finalError = error;
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
for (const fn of interceptors.error.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
finalError = (await fn(error, response, request, opts)) as string;
|
finalError = (await fn(error, response, request, opts)) as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalError = finalError || ({} as string);
|
finalError = finalError || ({} as string);
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
if (opts.throwOnError) {
|
||||||
throw finalError;
|
throw finalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we probably want to return error and improve types
|
// TODO: we probably want to return error and improve types
|
||||||
return opts.responseStyle === "data"
|
return opts.responseStyle === 'data'
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
error: finalError,
|
error: finalError,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => request({ ...options, method });
|
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
||||||
|
request({ ...options, method });
|
||||||
|
|
||||||
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
||||||
const { opts, url } = await beforeRequest(options);
|
const { opts, url } = await beforeRequest(options);
|
||||||
return createSseClient({
|
return createSseClient({
|
||||||
...opts,
|
...opts,
|
||||||
body: opts.body as BodyInit | null | undefined,
|
body: opts.body as BodyInit | null | undefined,
|
||||||
headers: opts.headers as unknown as Record<string, string>,
|
headers: opts.headers as unknown as Record<string, string>,
|
||||||
method,
|
method,
|
||||||
onRequest: async (url, init) => {
|
onRequest: async (url, init) => {
|
||||||
let request = new Request(url, init);
|
let request = new Request(url, init);
|
||||||
for (const fn of interceptors.request.fns) {
|
for (const fn of interceptors.request.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
request = await fn(request, opts);
|
request = await fn(request, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
|
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buildUrl,
|
buildUrl,
|
||||||
connect: makeMethodFn("CONNECT"),
|
connect: makeMethodFn('CONNECT'),
|
||||||
delete: makeMethodFn("DELETE"),
|
delete: makeMethodFn('DELETE'),
|
||||||
get: makeMethodFn("GET"),
|
get: makeMethodFn('GET'),
|
||||||
getConfig,
|
getConfig,
|
||||||
head: makeMethodFn("HEAD"),
|
head: makeMethodFn('HEAD'),
|
||||||
interceptors,
|
interceptors,
|
||||||
options: makeMethodFn("OPTIONS"),
|
options: makeMethodFn('OPTIONS'),
|
||||||
patch: makeMethodFn("PATCH"),
|
patch: makeMethodFn('PATCH'),
|
||||||
post: makeMethodFn("POST"),
|
post: makeMethodFn('POST'),
|
||||||
put: makeMethodFn("PUT"),
|
put: makeMethodFn('PUT'),
|
||||||
request,
|
request,
|
||||||
setConfig,
|
setConfig,
|
||||||
sse: {
|
sse: {
|
||||||
connect: makeSseFn("CONNECT"),
|
connect: makeSseFn('CONNECT'),
|
||||||
delete: makeSseFn("DELETE"),
|
delete: makeSseFn('DELETE'),
|
||||||
get: makeSseFn("GET"),
|
get: makeSseFn('GET'),
|
||||||
head: makeSseFn("HEAD"),
|
head: makeSseFn('HEAD'),
|
||||||
options: makeSseFn("OPTIONS"),
|
options: makeSseFn('OPTIONS'),
|
||||||
patch: makeSseFn("PATCH"),
|
patch: makeSseFn('PATCH'),
|
||||||
post: makeSseFn("POST"),
|
post: makeSseFn('POST'),
|
||||||
put: makeSseFn("PUT"),
|
put: makeSseFn('PUT'),
|
||||||
trace: makeSseFn("TRACE"),
|
trace: makeSseFn('TRACE'),
|
||||||
},
|
},
|
||||||
trace: makeMethodFn("TRACE"),
|
trace: makeMethodFn('TRACE'),
|
||||||
} as Client;
|
} as Client;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,176 +1,184 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Auth } from "../core/auth.gen";
|
import type { Auth } from '../core/auth.gen';
|
||||||
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen";
|
import type {
|
||||||
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen";
|
ServerSentEventsOptions,
|
||||||
import type { Middleware } from "./utils.gen";
|
ServerSentEventsResult,
|
||||||
|
} from '../core/serverSentEvents.gen';
|
||||||
|
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
|
||||||
|
import type { Middleware } from './utils.gen';
|
||||||
|
|
||||||
export type ResponseStyle = "data" | "fields";
|
export type ResponseStyle = 'data' | 'fields';
|
||||||
|
|
||||||
export interface Config<T extends ClientOptions = ClientOptions>
|
export interface Config<T extends ClientOptions = ClientOptions>
|
||||||
extends Omit<RequestInit, "body" | "headers" | "method">, CoreConfig {
|
extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
|
||||||
/**
|
/**
|
||||||
* Base URL for all requests made by this client.
|
* Base URL for all requests made by this client.
|
||||||
*/
|
*/
|
||||||
baseUrl?: T["baseUrl"];
|
baseUrl?: T['baseUrl'];
|
||||||
/**
|
/**
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
* Fetch API implementation. You can use this option to provide a custom
|
||||||
* fetch instance.
|
* fetch instance.
|
||||||
*
|
*
|
||||||
* @default globalThis.fetch
|
* @default globalThis.fetch
|
||||||
*/
|
*/
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
/**
|
/**
|
||||||
* Please don't use the Fetch client for Next.js applications. The `next`
|
* Please don't use the Fetch client for Next.js applications. The `next`
|
||||||
* options won't have any effect.
|
* options won't have any effect.
|
||||||
*
|
*
|
||||||
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
||||||
*/
|
*/
|
||||||
next?: never;
|
next?: never;
|
||||||
/**
|
/**
|
||||||
* Return the response data parsed in a specified format. By default, `auto`
|
* Return the response data parsed in a specified format. By default, `auto`
|
||||||
* will infer the appropriate method from the `Content-Type` response header.
|
* will infer the appropriate method from the `Content-Type` response header.
|
||||||
* You can override this behavior with any of the {@link Body} methods.
|
* You can override this behavior with any of the {@link Body} methods.
|
||||||
* Select `stream` if you don't want to parse response data at all.
|
* Select `stream` if you don't want to parse response data at all.
|
||||||
*
|
*
|
||||||
* @default 'auto'
|
* @default 'auto'
|
||||||
*/
|
*/
|
||||||
parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text";
|
parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
|
||||||
/**
|
/**
|
||||||
* Should we return only data or multiple fields (data, error, response, etc.)?
|
* Should we return only data or multiple fields (data, error, response, etc.)?
|
||||||
*
|
*
|
||||||
* @default 'fields'
|
* @default 'fields'
|
||||||
*/
|
*/
|
||||||
responseStyle?: ResponseStyle;
|
responseStyle?: ResponseStyle;
|
||||||
/**
|
/**
|
||||||
* Throw an error instead of returning it in the response?
|
* Throw an error instead of returning it in the response?
|
||||||
*
|
*
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
throwOnError?: T["throwOnError"];
|
throwOnError?: T['throwOnError'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestOptions<
|
export interface RequestOptions<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
Url extends string = string,
|
Url extends string = string,
|
||||||
>
|
>
|
||||||
extends
|
extends
|
||||||
Config<{
|
Config<{
|
||||||
responseStyle: TResponseStyle;
|
responseStyle: TResponseStyle;
|
||||||
throwOnError: ThrowOnError;
|
throwOnError: ThrowOnError;
|
||||||
}>,
|
}>,
|
||||||
Pick<
|
Pick<
|
||||||
ServerSentEventsOptions<TData>,
|
ServerSentEventsOptions<TData>,
|
||||||
"onRequest" | "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
|
| 'onRequest'
|
||||||
> {
|
| 'onSseError'
|
||||||
/**
|
| 'onSseEvent'
|
||||||
* Any body that you want to add to your request.
|
| 'sseDefaultRetryDelay'
|
||||||
*
|
| 'sseMaxRetryAttempts'
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
| 'sseMaxRetryDelay'
|
||||||
*/
|
> {
|
||||||
body?: unknown;
|
/**
|
||||||
path?: Record<string, unknown>;
|
* Any body that you want to add to your request.
|
||||||
query?: Record<string, unknown>;
|
*
|
||||||
/**
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
||||||
* Security mechanism(s) to use for the request.
|
*/
|
||||||
*/
|
body?: unknown;
|
||||||
security?: ReadonlyArray<Auth>;
|
path?: Record<string, unknown>;
|
||||||
url: Url;
|
query?: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Security mechanism(s) to use for the request.
|
||||||
|
*/
|
||||||
|
security?: ReadonlyArray<Auth>;
|
||||||
|
url: Url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolvedRequestOptions<
|
export interface ResolvedRequestOptions<
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
Url extends string = string,
|
Url extends string = string,
|
||||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
||||||
serializedBody?: string;
|
serializedBody?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestResult<
|
export type RequestResult<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
> = ThrowOnError extends true
|
> = ThrowOnError extends true
|
||||||
? Promise<
|
? Promise<
|
||||||
TResponseStyle extends "data"
|
TResponseStyle extends 'data'
|
||||||
? TData extends Record<string, unknown>
|
? TData extends Record<string, unknown>
|
||||||
? TData[keyof TData]
|
? TData[keyof TData]
|
||||||
: TData
|
: TData
|
||||||
: {
|
: {
|
||||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
||||||
request: Request;
|
request: Request;
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
: Promise<
|
: Promise<
|
||||||
TResponseStyle extends "data"
|
TResponseStyle extends 'data'
|
||||||
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
|
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
|
||||||
: (
|
: (
|
||||||
| {
|
| {
|
||||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
||||||
error: undefined;
|
error: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: undefined;
|
data: undefined;
|
||||||
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
|
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
|
||||||
}
|
}
|
||||||
) & {
|
) & {
|
||||||
request: Request;
|
request: Request;
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface ClientOptions {
|
export interface ClientOptions {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
responseStyle?: ResponseStyle;
|
responseStyle?: ResponseStyle;
|
||||||
throwOnError?: boolean;
|
throwOnError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MethodFn = <
|
type MethodFn = <
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||||
|
|
||||||
type SseFn = <
|
type SseFn = <
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
||||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
) => Promise<ServerSentEventsResult<TData, TError>>;
|
||||||
|
|
||||||
type RequestFn = <
|
type RequestFn = <
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
|
||||||
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
|
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||||
|
|
||||||
type BuildUrlFn = <
|
type BuildUrlFn = <
|
||||||
TData extends {
|
TData extends {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
path?: Record<string, unknown>;
|
path?: Record<string, unknown>;
|
||||||
query?: Record<string, unknown>;
|
query?: Record<string, unknown>;
|
||||||
url: string;
|
url: string;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
options: TData & Options<TData>,
|
options: TData & Options<TData>,
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
|
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
|
||||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -182,23 +190,26 @@ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn>
|
||||||
* to ensure your client always has the correct values.
|
* to ensure your client always has the correct values.
|
||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||||
override?: Config<ClientOptions & T>,
|
override?: Config<ClientOptions & T>,
|
||||||
) => Config<Required<ClientOptions> & T>;
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export interface TDataShape {
|
export interface TDataShape {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
headers?: unknown;
|
headers?: unknown;
|
||||||
path?: unknown;
|
path?: unknown;
|
||||||
query?: unknown;
|
query?: unknown;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||||
|
|
||||||
export type Options<
|
export type Options<
|
||||||
TData extends TDataShape = TDataShape,
|
TData extends TDataShape = TDataShape,
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
TResponse = unknown,
|
TResponse = unknown,
|
||||||
TResponseStyle extends ResponseStyle = "fields",
|
TResponseStyle extends ResponseStyle = 'fields',
|
||||||
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
|
> = OmitKeys<
|
||||||
([TData] extends [never] ? unknown : Omit<TData, "url">);
|
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
|
||||||
|
'body' | 'path' | 'query' | 'url'
|
||||||
|
> &
|
||||||
|
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
|
||||||
|
|
|
||||||
|
|
@ -1,290 +1,317 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { getAuthToken } from "../core/auth.gen";
|
import { getAuthToken } from '../core/auth.gen';
|
||||||
import type { QuerySerializerOptions } from "../core/bodySerializer.gen";
|
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
||||||
import { jsonBodySerializer } from "../core/bodySerializer.gen";
|
import { jsonBodySerializer } from '../core/bodySerializer.gen';
|
||||||
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen";
|
import {
|
||||||
import { getUrl } from "../core/utils.gen";
|
serializeArrayParam,
|
||||||
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen";
|
serializeObjectParam,
|
||||||
|
serializePrimitiveParam,
|
||||||
|
} from '../core/pathSerializer.gen';
|
||||||
|
import { getUrl } from '../core/utils.gen';
|
||||||
|
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
|
||||||
|
|
||||||
export const createQuerySerializer = <T = unknown>({ parameters = {}, ...args }: QuerySerializerOptions = {}) => {
|
export const createQuerySerializer = <T = unknown>({
|
||||||
const querySerializer = (queryParams: T) => {
|
parameters = {},
|
||||||
const search: string[] = [];
|
...args
|
||||||
if (queryParams && typeof queryParams === "object") {
|
}: QuerySerializerOptions = {}) => {
|
||||||
for (const name in queryParams) {
|
const querySerializer = (queryParams: T) => {
|
||||||
const value = queryParams[name];
|
const search: string[] = [];
|
||||||
|
if (queryParams && typeof queryParams === 'object') {
|
||||||
|
for (const name in queryParams) {
|
||||||
|
const value = queryParams[name];
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = parameters[name] || args;
|
const options = parameters[name] || args;
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const serializedArray = serializeArrayParam({
|
const serializedArray = serializeArrayParam({
|
||||||
allowReserved: options.allowReserved,
|
allowReserved: options.allowReserved,
|
||||||
explode: true,
|
explode: true,
|
||||||
name,
|
name,
|
||||||
style: "form",
|
style: 'form',
|
||||||
value,
|
value,
|
||||||
...options.array,
|
...options.array,
|
||||||
});
|
});
|
||||||
if (serializedArray) search.push(serializedArray);
|
if (serializedArray) search.push(serializedArray);
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === 'object') {
|
||||||
const serializedObject = serializeObjectParam({
|
const serializedObject = serializeObjectParam({
|
||||||
allowReserved: options.allowReserved,
|
allowReserved: options.allowReserved,
|
||||||
explode: true,
|
explode: true,
|
||||||
name,
|
name,
|
||||||
style: "deepObject",
|
style: 'deepObject',
|
||||||
value: value as Record<string, unknown>,
|
value: value as Record<string, unknown>,
|
||||||
...options.object,
|
...options.object,
|
||||||
});
|
});
|
||||||
if (serializedObject) search.push(serializedObject);
|
if (serializedObject) search.push(serializedObject);
|
||||||
} else {
|
} else {
|
||||||
const serializedPrimitive = serializePrimitiveParam({
|
const serializedPrimitive = serializePrimitiveParam({
|
||||||
allowReserved: options.allowReserved,
|
allowReserved: options.allowReserved,
|
||||||
name,
|
name,
|
||||||
value: value as string,
|
value: value as string,
|
||||||
});
|
});
|
||||||
if (serializedPrimitive) search.push(serializedPrimitive);
|
if (serializedPrimitive) search.push(serializedPrimitive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return search.join("&");
|
return search.join('&');
|
||||||
};
|
};
|
||||||
return querySerializer;
|
return querySerializer;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infers parseAs value from provided Content-Type header.
|
* Infers parseAs value from provided Content-Type header.
|
||||||
*/
|
*/
|
||||||
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => {
|
export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => {
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
||||||
// which is effectively the same as the 'stream' option.
|
// which is effectively the same as the 'stream' option.
|
||||||
return "stream";
|
return 'stream';
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanContent = contentType.split(";")[0]?.trim();
|
const cleanContent = contentType.split(';')[0]?.trim();
|
||||||
|
|
||||||
if (!cleanContent) {
|
if (!cleanContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) {
|
if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) {
|
||||||
return "json";
|
return 'json';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanContent === "multipart/form-data") {
|
if (cleanContent === 'multipart/form-data') {
|
||||||
return "formData";
|
return 'formData';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) {
|
if (
|
||||||
return "blob";
|
['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type))
|
||||||
}
|
) {
|
||||||
|
return 'blob';
|
||||||
|
}
|
||||||
|
|
||||||
if (cleanContent.startsWith("text/")) {
|
if (cleanContent.startsWith('text/')) {
|
||||||
return "text";
|
return 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForExistence = (
|
const checkForExistence = (
|
||||||
options: Pick<RequestOptions, "auth" | "query"> & {
|
options: Pick<RequestOptions, 'auth' | 'query'> & {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
},
|
},
|
||||||
name?: string,
|
name?: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
|
if (
|
||||||
return true;
|
options.headers.has(name) ||
|
||||||
}
|
options.query?.[name] ||
|
||||||
return false;
|
options.headers.get('Cookie')?.includes(`${name}=`)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAuthParams = async ({
|
export const setAuthParams = async ({
|
||||||
security,
|
security,
|
||||||
...options
|
...options
|
||||||
}: Pick<Required<RequestOptions>, "security"> &
|
}: Pick<Required<RequestOptions>, 'security'> &
|
||||||
Pick<RequestOptions, "auth" | "query"> & {
|
Pick<RequestOptions, 'auth' | 'query'> & {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
}) => {
|
}) => {
|
||||||
for (const auth of security) {
|
for (const auth of security) {
|
||||||
if (checkForExistence(options, auth.name)) {
|
if (checkForExistence(options, auth.name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getAuthToken(auth, options.auth);
|
const token = await getAuthToken(auth, options.auth);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = auth.name ?? "Authorization";
|
const name = auth.name ?? 'Authorization';
|
||||||
|
|
||||||
switch (auth.in) {
|
switch (auth.in) {
|
||||||
case "query":
|
case 'query':
|
||||||
if (!options.query) {
|
if (!options.query) {
|
||||||
options.query = {};
|
options.query = {};
|
||||||
}
|
}
|
||||||
options.query[name] = token;
|
options.query[name] = token;
|
||||||
break;
|
break;
|
||||||
case "cookie":
|
case 'cookie':
|
||||||
options.headers.append("Cookie", `${name}=${token}`);
|
options.headers.append('Cookie', `${name}=${token}`);
|
||||||
break;
|
break;
|
||||||
case "header":
|
case 'header':
|
||||||
default:
|
default:
|
||||||
options.headers.set(name, token);
|
options.headers.set(name, token);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildUrl: Client["buildUrl"] = (options) =>
|
export const buildUrl: Client['buildUrl'] = (options) =>
|
||||||
getUrl({
|
getUrl({
|
||||||
baseUrl: options.baseUrl as string,
|
baseUrl: options.baseUrl as string,
|
||||||
path: options.path,
|
path: options.path,
|
||||||
query: options.query,
|
query: options.query,
|
||||||
querySerializer:
|
querySerializer:
|
||||||
typeof options.querySerializer === "function"
|
typeof options.querySerializer === 'function'
|
||||||
? options.querySerializer
|
? options.querySerializer
|
||||||
: createQuerySerializer(options.querySerializer),
|
: createQuerySerializer(options.querySerializer),
|
||||||
url: options.url,
|
url: options.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mergeConfigs = (a: Config, b: Config): Config => {
|
export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||||
const config = { ...a, ...b };
|
const config = { ...a, ...b };
|
||||||
if (config.baseUrl?.endsWith("/")) {
|
if (config.baseUrl?.endsWith('/')) {
|
||||||
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
||||||
}
|
}
|
||||||
config.headers = mergeHeaders(a.headers, b.headers);
|
config.headers = mergeHeaders(a.headers, b.headers);
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
||||||
const entries: Array<[string, string]> = [];
|
const entries: Array<[string, string]> = [];
|
||||||
headers.forEach((value, key) => {
|
headers.forEach((value, key) => {
|
||||||
entries.push([key, value]);
|
entries.push([key, value]);
|
||||||
});
|
});
|
||||||
return entries;
|
return entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => {
|
export const mergeHeaders = (
|
||||||
const mergedHeaders = new Headers();
|
...headers: Array<Required<Config>['headers'] | undefined>
|
||||||
for (const header of headers) {
|
): Headers => {
|
||||||
if (!header) {
|
const mergedHeaders = new Headers();
|
||||||
continue;
|
for (const header of headers) {
|
||||||
}
|
if (!header) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
||||||
|
|
||||||
for (const [key, value] of iterator) {
|
for (const [key, value] of iterator) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
mergedHeaders.delete(key);
|
mergedHeaders.delete(key);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
for (const v of value) {
|
for (const v of value) {
|
||||||
mergedHeaders.append(key, v as string);
|
mergedHeaders.append(key, v as string);
|
||||||
}
|
}
|
||||||
} else if (value !== undefined) {
|
} else if (value !== undefined) {
|
||||||
// assume object headers are meant to be JSON stringified, i.e. their
|
// assume object headers are meant to be JSON stringified, i.e. their
|
||||||
// content value in OpenAPI specification is 'application/json'
|
// content value in OpenAPI specification is 'application/json'
|
||||||
mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string));
|
mergedHeaders.set(
|
||||||
}
|
key,
|
||||||
}
|
typeof value === 'object' ? JSON.stringify(value) : (value as string),
|
||||||
}
|
);
|
||||||
return mergedHeaders;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedHeaders;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ErrInterceptor<Err, Res, Req, Options> = (
|
type ErrInterceptor<Err, Res, Req, Options> = (
|
||||||
error: Err,
|
error: Err,
|
||||||
response: Res,
|
response: Res,
|
||||||
request: Req,
|
request: Req,
|
||||||
options: Options,
|
options: Options,
|
||||||
) => Err | Promise<Err>;
|
) => Err | Promise<Err>;
|
||||||
|
|
||||||
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
|
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
|
||||||
|
|
||||||
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>;
|
type ResInterceptor<Res, Req, Options> = (
|
||||||
|
response: Res,
|
||||||
|
request: Req,
|
||||||
|
options: Options,
|
||||||
|
) => Res | Promise<Res>;
|
||||||
|
|
||||||
class Interceptors<Interceptor> {
|
class Interceptors<Interceptor> {
|
||||||
fns: Array<Interceptor | null> = [];
|
fns: Array<Interceptor | null> = [];
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.fns = [];
|
this.fns = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
eject(id: number | Interceptor): void {
|
eject(id: number | Interceptor): void {
|
||||||
const index = this.getInterceptorIndex(id);
|
const index = this.getInterceptorIndex(id);
|
||||||
if (this.fns[index]) {
|
if (this.fns[index]) {
|
||||||
this.fns[index] = null;
|
this.fns[index] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exists(id: number | Interceptor): boolean {
|
exists(id: number | Interceptor): boolean {
|
||||||
const index = this.getInterceptorIndex(id);
|
const index = this.getInterceptorIndex(id);
|
||||||
return Boolean(this.fns[index]);
|
return Boolean(this.fns[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInterceptorIndex(id: number | Interceptor): number {
|
getInterceptorIndex(id: number | Interceptor): number {
|
||||||
if (typeof id === "number") {
|
if (typeof id === 'number') {
|
||||||
return this.fns[id] ? id : -1;
|
return this.fns[id] ? id : -1;
|
||||||
}
|
}
|
||||||
return this.fns.indexOf(id);
|
return this.fns.indexOf(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
|
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
|
||||||
const index = this.getInterceptorIndex(id);
|
const index = this.getInterceptorIndex(id);
|
||||||
if (this.fns[index]) {
|
if (this.fns[index]) {
|
||||||
this.fns[index] = fn;
|
this.fns[index] = fn;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
use(fn: Interceptor): number {
|
use(fn: Interceptor): number {
|
||||||
this.fns.push(fn);
|
this.fns.push(fn);
|
||||||
return this.fns.length - 1;
|
return this.fns.length - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Middleware<Req, Res, Err, Options> {
|
export interface Middleware<Req, Res, Err, Options> {
|
||||||
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
||||||
request: Interceptors<ReqInterceptor<Req, Options>>;
|
request: Interceptors<ReqInterceptor<Req, Options>>;
|
||||||
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<Req, Res, Err, Options> => ({
|
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
|
||||||
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
Req,
|
||||||
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
Res,
|
||||||
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
Err,
|
||||||
|
Options
|
||||||
|
> => ({
|
||||||
|
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
||||||
|
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
||||||
|
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultQuerySerializer = createQuerySerializer({
|
const defaultQuerySerializer = createQuerySerializer({
|
||||||
allowReserved: false,
|
allowReserved: false,
|
||||||
array: {
|
array: {
|
||||||
explode: true,
|
explode: true,
|
||||||
style: "form",
|
style: 'form',
|
||||||
},
|
},
|
||||||
object: {
|
object: {
|
||||||
explode: true,
|
explode: true,
|
||||||
style: "deepObject",
|
style: 'deepObject',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||||
...jsonBodySerializer,
|
...jsonBodySerializer,
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
parseAs: "auto",
|
parseAs: 'auto',
|
||||||
querySerializer: defaultQuerySerializer,
|
querySerializer: defaultQuerySerializer,
|
||||||
...override,
|
...override,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,39 +4,39 @@
|
||||||
export type AuthToken = string | undefined;
|
export type AuthToken = string | undefined;
|
||||||
|
|
||||||
export interface Auth {
|
export interface Auth {
|
||||||
/**
|
/**
|
||||||
* Which part of the request do we use to send the auth?
|
* Which part of the request do we use to send the auth?
|
||||||
*
|
*
|
||||||
* @default 'header'
|
* @default 'header'
|
||||||
*/
|
*/
|
||||||
in?: "header" | "query" | "cookie";
|
in?: 'header' | 'query' | 'cookie';
|
||||||
/**
|
/**
|
||||||
* Header or query parameter name.
|
* Header or query parameter name.
|
||||||
*
|
*
|
||||||
* @default 'Authorization'
|
* @default 'Authorization'
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
scheme?: "basic" | "bearer";
|
scheme?: 'basic' | 'bearer';
|
||||||
type: "apiKey" | "http";
|
type: 'apiKey' | 'http';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAuthToken = async (
|
export const getAuthToken = async (
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const token = typeof callback === "function" ? await callback(auth) : callback;
|
const token = typeof callback === 'function' ? await callback(auth) : callback;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.scheme === "bearer") {
|
if (auth.scheme === 'bearer') {
|
||||||
return `Bearer ${token}`;
|
return `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.scheme === "basic") {
|
if (auth.scheme === 'basic') {
|
||||||
return `Basic ${btoa(token)}`;
|
return `Basic ${btoa(token)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,85 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen";
|
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
|
||||||
|
|
||||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||||
|
|
||||||
export type BodySerializer = (body: any) => any;
|
export type BodySerializer = (body: any) => any;
|
||||||
|
|
||||||
type QuerySerializerOptionsObject = {
|
type QuerySerializerOptionsObject = {
|
||||||
allowReserved?: boolean;
|
allowReserved?: boolean;
|
||||||
array?: Partial<SerializerOptions<ArrayStyle>>;
|
array?: Partial<SerializerOptions<ArrayStyle>>;
|
||||||
object?: Partial<SerializerOptions<ObjectStyle>>;
|
object?: Partial<SerializerOptions<ObjectStyle>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
||||||
/**
|
/**
|
||||||
* Per-parameter serialization overrides. When provided, these settings
|
* Per-parameter serialization overrides. When provided, these settings
|
||||||
* override the global array/object settings for specific parameter names.
|
* override the global array/object settings for specific parameter names.
|
||||||
*/
|
*/
|
||||||
parameters?: Record<string, QuerySerializerOptionsObject>;
|
parameters?: Record<string, QuerySerializerOptionsObject>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
|
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
|
||||||
if (typeof value === "string" || value instanceof Blob) {
|
if (typeof value === 'string' || value instanceof Blob) {
|
||||||
data.append(key, value);
|
data.append(key, value);
|
||||||
} else if (value instanceof Date) {
|
} else if (value instanceof Date) {
|
||||||
data.append(key, value.toISOString());
|
data.append(key, value.toISOString());
|
||||||
} else {
|
} else {
|
||||||
data.append(key, JSON.stringify(value));
|
data.append(key, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
|
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
|
||||||
if (typeof value === "string") {
|
if (typeof value === 'string') {
|
||||||
data.append(key, value);
|
data.append(key, value);
|
||||||
} else {
|
} else {
|
||||||
data.append(key, JSON.stringify(value));
|
data.append(key, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formDataBodySerializer = {
|
export const formDataBodySerializer = {
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): FormData => {
|
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||||
const data = new FormData();
|
body: T,
|
||||||
|
): FormData => {
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
Object.entries(body).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((v) => serializeFormDataPair(data, key, v));
|
value.forEach((v) => serializeFormDataPair(data, key, v));
|
||||||
} else {
|
} else {
|
||||||
serializeFormDataPair(data, key, value);
|
serializeFormDataPair(data, key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jsonBodySerializer = {
|
export const jsonBodySerializer = {
|
||||||
bodySerializer: <T>(body: T): string =>
|
bodySerializer: <T>(body: T): string =>
|
||||||
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)),
|
JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const urlSearchParamsBodySerializer = {
|
export const urlSearchParamsBodySerializer = {
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => {
|
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => {
|
||||||
const data = new URLSearchParams();
|
const data = new URLSearchParams();
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
Object.entries(body).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
||||||
} else {
|
} else {
|
||||||
serializeUrlSearchParamsPair(data, key, value);
|
serializeUrlSearchParamsPair(data, key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.toString();
|
return data.toString();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,170 +1,170 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
type Slot = "body" | "headers" | "path" | "query";
|
type Slot = 'body' | 'headers' | 'path' | 'query';
|
||||||
|
|
||||||
export type Field =
|
export type Field =
|
||||||
| {
|
| {
|
||||||
in: Exclude<Slot, "body">;
|
in: Exclude<Slot, 'body'>;
|
||||||
/**
|
/**
|
||||||
* Field name. This is the name we want the user to see and use.
|
* Field name. This is the name we want the user to see and use.
|
||||||
*/
|
*/
|
||||||
key: string;
|
key: string;
|
||||||
/**
|
/**
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
* If omitted, we use the same value as `key`.
|
* If omitted, we use the same value as `key`.
|
||||||
*/
|
*/
|
||||||
map?: string;
|
map?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
in: Extract<Slot, "body">;
|
in: Extract<Slot, 'body'>;
|
||||||
/**
|
/**
|
||||||
* Key isn't required for bodies.
|
* Key isn't required for bodies.
|
||||||
*/
|
*/
|
||||||
key?: string;
|
key?: string;
|
||||||
map?: string;
|
map?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
/**
|
/**
|
||||||
* Field name. This is the name we want the user to see and use.
|
* Field name. This is the name we want the user to see and use.
|
||||||
*/
|
*/
|
||||||
key: string;
|
key: string;
|
||||||
/**
|
/**
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
||||||
*/
|
*/
|
||||||
map: Slot;
|
map: Slot;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Fields {
|
export interface Fields {
|
||||||
allowExtra?: Partial<Record<Slot, boolean>>;
|
allowExtra?: Partial<Record<Slot, boolean>>;
|
||||||
args?: ReadonlyArray<Field>;
|
args?: ReadonlyArray<Field>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
||||||
|
|
||||||
const extraPrefixesMap: Record<string, Slot> = {
|
const extraPrefixesMap: Record<string, Slot> = {
|
||||||
$body_: "body",
|
$body_: 'body',
|
||||||
$headers_: "headers",
|
$headers_: 'headers',
|
||||||
$path_: "path",
|
$path_: 'path',
|
||||||
$query_: "query",
|
$query_: 'query',
|
||||||
};
|
};
|
||||||
const extraPrefixes = Object.entries(extraPrefixesMap);
|
const extraPrefixes = Object.entries(extraPrefixesMap);
|
||||||
|
|
||||||
type KeyMap = Map<
|
type KeyMap = Map<
|
||||||
string,
|
string,
|
||||||
| {
|
| {
|
||||||
in: Slot;
|
in: Slot;
|
||||||
map?: string;
|
map?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
in?: never;
|
in?: never;
|
||||||
map: Slot;
|
map: Slot;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
map = new Map();
|
map = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const config of fields) {
|
for (const config of fields) {
|
||||||
if ("in" in config) {
|
if ('in' in config) {
|
||||||
if (config.key) {
|
if (config.key) {
|
||||||
map.set(config.key, {
|
map.set(config.key, {
|
||||||
in: config.in,
|
in: config.in,
|
||||||
map: config.map,
|
map: config.map,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if ("key" in config) {
|
} else if ('key' in config) {
|
||||||
map.set(config.key, {
|
map.set(config.key, {
|
||||||
map: config.map,
|
map: config.map,
|
||||||
});
|
});
|
||||||
} else if (config.args) {
|
} else if (config.args) {
|
||||||
buildKeyMap(config.args, map);
|
buildKeyMap(config.args, map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
body: unknown;
|
body: unknown;
|
||||||
headers: Record<string, unknown>;
|
headers: Record<string, unknown>;
|
||||||
path: Record<string, unknown>;
|
path: Record<string, unknown>;
|
||||||
query: Record<string, unknown>;
|
query: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripEmptySlots = (params: Params) => {
|
const stripEmptySlots = (params: Params) => {
|
||||||
for (const [slot, value] of Object.entries(params)) {
|
for (const [slot, value] of Object.entries(params)) {
|
||||||
if (value && typeof value === "object" && !Object.keys(value).length) {
|
if (value && typeof value === 'object' && !Object.keys(value).length) {
|
||||||
delete params[slot as Slot];
|
delete params[slot as Slot];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
|
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
|
||||||
const params: Params = {
|
const params: Params = {
|
||||||
body: {},
|
body: {},
|
||||||
headers: {},
|
headers: {},
|
||||||
path: {},
|
path: {},
|
||||||
query: {},
|
query: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const map = buildKeyMap(fields);
|
const map = buildKeyMap(fields);
|
||||||
|
|
||||||
let config: FieldsConfig[number] | undefined;
|
let config: FieldsConfig[number] | undefined;
|
||||||
|
|
||||||
for (const [index, arg] of args.entries()) {
|
for (const [index, arg] of args.entries()) {
|
||||||
if (fields[index]) {
|
if (fields[index]) {
|
||||||
config = fields[index];
|
config = fields[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("in" in config) {
|
if ('in' in config) {
|
||||||
if (config.key) {
|
if (config.key) {
|
||||||
const field = map.get(config.key)!;
|
const field = map.get(config.key)!;
|
||||||
const name = field.map || config.key;
|
const name = field.map || config.key;
|
||||||
if (field.in) {
|
if (field.in) {
|
||||||
(params[field.in] as Record<string, unknown>)[name] = arg;
|
(params[field.in] as Record<string, unknown>)[name] = arg;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
params.body = arg;
|
params.body = arg;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [key, value] of Object.entries(arg ?? {})) {
|
for (const [key, value] of Object.entries(arg ?? {})) {
|
||||||
const field = map.get(key);
|
const field = map.get(key);
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
if (field.in) {
|
if (field.in) {
|
||||||
const name = field.map || key;
|
const name = field.map || key;
|
||||||
(params[field.in] as Record<string, unknown>)[name] = value;
|
(params[field.in] as Record<string, unknown>)[name] = value;
|
||||||
} else {
|
} else {
|
||||||
params[field.map] = value;
|
params[field.map] = value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
|
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
const [prefix, slot] = extra;
|
const [prefix, slot] = extra;
|
||||||
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
|
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
|
||||||
} else if ("allowExtra" in config && config.allowExtra) {
|
} else if ('allowExtra' in config && config.allowExtra) {
|
||||||
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stripEmptySlots(params);
|
stripEmptySlots(params);
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,165 +4,169 @@
|
||||||
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||||
|
|
||||||
interface SerializePrimitiveOptions {
|
interface SerializePrimitiveOptions {
|
||||||
allowReserved?: boolean;
|
allowReserved?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerializerOptions<T> {
|
export interface SerializerOptions<T> {
|
||||||
/**
|
/**
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
explode: boolean;
|
explode: boolean;
|
||||||
style: T;
|
style: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
|
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
||||||
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
||||||
type MatrixStyle = "label" | "matrix" | "simple";
|
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
||||||
export type ObjectStyle = "form" | "deepObject";
|
export type ObjectStyle = 'form' | 'deepObject';
|
||||||
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
||||||
|
|
||||||
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case "label":
|
case 'label':
|
||||||
return ".";
|
return '.';
|
||||||
case "matrix":
|
case 'matrix':
|
||||||
return ";";
|
return ';';
|
||||||
case "simple":
|
case 'simple':
|
||||||
return ",";
|
return ',';
|
||||||
default:
|
default:
|
||||||
return "&";
|
return '&';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case "form":
|
case 'form':
|
||||||
return ",";
|
return ',';
|
||||||
case "pipeDelimited":
|
case 'pipeDelimited':
|
||||||
return "|";
|
return '|';
|
||||||
case "spaceDelimited":
|
case 'spaceDelimited':
|
||||||
return "%20";
|
return '%20';
|
||||||
default:
|
default:
|
||||||
return ",";
|
return ',';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case "label":
|
case 'label':
|
||||||
return ".";
|
return '.';
|
||||||
case "matrix":
|
case 'matrix':
|
||||||
return ";";
|
return ';';
|
||||||
case "simple":
|
case 'simple':
|
||||||
return ",";
|
return ',';
|
||||||
default:
|
default:
|
||||||
return "&";
|
return '&';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializeArrayParam = ({
|
export const serializeArrayParam = ({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
explode,
|
explode,
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
value,
|
value,
|
||||||
}: SerializeOptions<ArraySeparatorStyle> & {
|
}: SerializeOptions<ArraySeparatorStyle> & {
|
||||||
value: unknown[];
|
value: unknown[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!explode) {
|
if (!explode) {
|
||||||
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join(
|
const joinedValues = (
|
||||||
separatorArrayNoExplode(style),
|
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
||||||
);
|
).join(separatorArrayNoExplode(style));
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case "label":
|
case 'label':
|
||||||
return `.${joinedValues}`;
|
return `.${joinedValues}`;
|
||||||
case "matrix":
|
case 'matrix':
|
||||||
return `;${name}=${joinedValues}`;
|
return `;${name}=${joinedValues}`;
|
||||||
case "simple":
|
case 'simple':
|
||||||
return joinedValues;
|
return joinedValues;
|
||||||
default:
|
default:
|
||||||
return `${name}=${joinedValues}`;
|
return `${name}=${joinedValues}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const separator = separatorArrayExplode(style);
|
const separator = separatorArrayExplode(style);
|
||||||
const joinedValues = value
|
const joinedValues = value
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
if (style === "label" || style === "simple") {
|
if (style === 'label' || style === 'simple') {
|
||||||
return allowReserved ? v : encodeURIComponent(v as string);
|
return allowReserved ? v : encodeURIComponent(v as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializePrimitiveParam({
|
return serializePrimitiveParam({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
name,
|
name,
|
||||||
value: v as string,
|
value: v as string,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.join(separator);
|
.join(separator);
|
||||||
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => {
|
export const serializePrimitiveParam = ({
|
||||||
if (value === undefined || value === null) {
|
allowReserved,
|
||||||
return "";
|
name,
|
||||||
}
|
value,
|
||||||
|
}: SerializePrimitiveParam) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (typeof value === 'object') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.",
|
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializeObjectParam = ({
|
export const serializeObjectParam = ({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
explode,
|
explode,
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
value,
|
value,
|
||||||
valueOnly,
|
valueOnly,
|
||||||
}: SerializeOptions<ObjectSeparatorStyle> & {
|
}: SerializeOptions<ObjectSeparatorStyle> & {
|
||||||
value: Record<string, unknown> | Date;
|
value: Record<string, unknown> | Date;
|
||||||
valueOnly?: boolean;
|
valueOnly?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style !== "deepObject" && !explode) {
|
if (style !== 'deepObject' && !explode) {
|
||||||
let values: string[] = [];
|
let values: string[] = [];
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
|
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
|
||||||
});
|
});
|
||||||
const joinedValues = values.join(",");
|
const joinedValues = values.join(',');
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case "form":
|
case 'form':
|
||||||
return `${name}=${joinedValues}`;
|
return `${name}=${joinedValues}`;
|
||||||
case "label":
|
case 'label':
|
||||||
return `.${joinedValues}`;
|
return `.${joinedValues}`;
|
||||||
case "matrix":
|
case 'matrix':
|
||||||
return `;${name}=${joinedValues}`;
|
return `;${name}=${joinedValues}`;
|
||||||
default:
|
default:
|
||||||
return joinedValues;
|
return joinedValues;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const separator = separatorObjectExplode(style);
|
const separator = separatorObjectExplode(style);
|
||||||
const joinedValues = Object.entries(value)
|
const joinedValues = Object.entries(value)
|
||||||
.map(([key, v]) =>
|
.map(([key, v]) =>
|
||||||
serializePrimitiveParam({
|
serializePrimitiveParam({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
name: style === "deepObject" ? `${name}[${key}]` : key,
|
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
||||||
value: v as string,
|
value: v as string,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.join(separator);
|
.join(separator);
|
||||||
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,109 +4,115 @@
|
||||||
/**
|
/**
|
||||||
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
||||||
*/
|
*/
|
||||||
export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue };
|
export type JsonValue =
|
||||||
|
| null
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| JsonValue[]
|
||||||
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
||||||
*/
|
*/
|
||||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
||||||
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
|
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (typeof value === "bigint") {
|
if (typeof value === 'bigint') {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toISOString();
|
return value.toISOString();
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely stringifies a value and parses it back into a JsonValue.
|
* Safely stringifies a value and parses it back into a JsonValue.
|
||||||
*/
|
*/
|
||||||
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
||||||
if (json === undefined) {
|
if (json === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return JSON.parse(json) as JsonValue;
|
return JSON.parse(json) as JsonValue;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects plain objects (including objects with a null prototype).
|
* Detects plain objects (including objects with a null prototype).
|
||||||
*/
|
*/
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||||
if (value === null || typeof value !== "object") {
|
if (value === null || typeof value !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const prototype = Object.getPrototypeOf(value as object);
|
const prototype = Object.getPrototypeOf(value as object);
|
||||||
return prototype === Object.prototype || prototype === null;
|
return prototype === Object.prototype || prototype === null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
||||||
*/
|
*/
|
||||||
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
||||||
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
|
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||||||
const result: Record<string, JsonValue> = {};
|
const result: Record<string, JsonValue> = {};
|
||||||
|
|
||||||
for (const [key, value] of entries) {
|
for (const [key, value] of entries) {
|
||||||
const existing = result[key];
|
const existing = result[key];
|
||||||
if (existing === undefined) {
|
if (existing === undefined) {
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(existing)) {
|
if (Array.isArray(existing)) {
|
||||||
(existing as string[]).push(value);
|
(existing as string[]).push(value);
|
||||||
} else {
|
} else {
|
||||||
result[key] = [existing, value];
|
result[key] = [existing, value];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
||||||
*/
|
*/
|
||||||
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
|
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
|
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "bigint") {
|
if (typeof value === 'bigint') {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toISOString();
|
return value.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return stringifyToJsonValue(value);
|
return stringifyToJsonValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) {
|
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
|
||||||
return serializeSearchParams(value);
|
return serializeSearchParams(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
if (isPlainObject(value)) {
|
||||||
return stringifyToJsonValue(value);
|
return stringifyToJsonValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,240 +1,244 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Config } from "./types.gen";
|
import type { Config } from './types.gen';
|
||||||
|
|
||||||
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
|
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
|
||||||
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
|
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
|
||||||
/**
|
/**
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
* Fetch API implementation. You can use this option to provide a custom
|
||||||
* fetch instance.
|
* fetch instance.
|
||||||
*
|
*
|
||||||
* @default globalThis.fetch
|
* @default globalThis.fetch
|
||||||
*/
|
*/
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
/**
|
/**
|
||||||
* Implementing clients can call request interceptors inside this hook.
|
* Implementing clients can call request interceptors inside this hook.
|
||||||
*/
|
*/
|
||||||
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
||||||
/**
|
/**
|
||||||
* Callback invoked when a network or parsing error occurs during streaming.
|
* Callback invoked when a network or parsing error occurs during streaming.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @param error The error that occurred.
|
* @param error The error that occurred.
|
||||||
*/
|
*/
|
||||||
onSseError?: (error: unknown) => void;
|
onSseError?: (error: unknown) => void;
|
||||||
/**
|
/**
|
||||||
* Callback invoked when an event is streamed from the server.
|
* Callback invoked when an event is streamed from the server.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @param event Event streamed from the server.
|
* @param event Event streamed from the server.
|
||||||
* @returns Nothing (void).
|
* @returns Nothing (void).
|
||||||
*/
|
*/
|
||||||
onSseEvent?: (event: StreamEvent<TData>) => void;
|
onSseEvent?: (event: StreamEvent<TData>) => void;
|
||||||
serializedBody?: RequestInit["body"];
|
serializedBody?: RequestInit['body'];
|
||||||
/**
|
/**
|
||||||
* Default retry delay in milliseconds.
|
* Default retry delay in milliseconds.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @default 3000
|
* @default 3000
|
||||||
*/
|
*/
|
||||||
sseDefaultRetryDelay?: number;
|
sseDefaultRetryDelay?: number;
|
||||||
/**
|
/**
|
||||||
* Maximum number of retry attempts before giving up.
|
* Maximum number of retry attempts before giving up.
|
||||||
*/
|
*/
|
||||||
sseMaxRetryAttempts?: number;
|
sseMaxRetryAttempts?: number;
|
||||||
/**
|
/**
|
||||||
* Maximum retry delay in milliseconds.
|
* Maximum retry delay in milliseconds.
|
||||||
*
|
*
|
||||||
* Applies only when exponential backoff is used.
|
* Applies only when exponential backoff is used.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @default 30000
|
* @default 30000
|
||||||
*/
|
*/
|
||||||
sseMaxRetryDelay?: number;
|
sseMaxRetryDelay?: number;
|
||||||
/**
|
/**
|
||||||
* Optional sleep function for retry backoff.
|
* Optional sleep function for retry backoff.
|
||||||
*
|
*
|
||||||
* Defaults to using `setTimeout`.
|
* Defaults to using `setTimeout`.
|
||||||
*/
|
*/
|
||||||
sseSleepFn?: (ms: number) => Promise<void>;
|
sseSleepFn?: (ms: number) => Promise<void>;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StreamEvent<TData = unknown> {
|
export interface StreamEvent<TData = unknown> {
|
||||||
data: TData;
|
data: TData;
|
||||||
event?: string;
|
event?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
retry?: number;
|
retry?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
||||||
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>;
|
stream: AsyncGenerator<
|
||||||
|
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
||||||
|
TReturn,
|
||||||
|
TNext
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSseClient = <TData = unknown>({
|
export const createSseClient = <TData = unknown>({
|
||||||
onRequest,
|
onRequest,
|
||||||
onSseError,
|
onSseError,
|
||||||
onSseEvent,
|
onSseEvent,
|
||||||
responseTransformer,
|
responseTransformer,
|
||||||
responseValidator,
|
responseValidator,
|
||||||
sseDefaultRetryDelay,
|
sseDefaultRetryDelay,
|
||||||
sseMaxRetryAttempts,
|
sseMaxRetryAttempts,
|
||||||
sseMaxRetryDelay,
|
sseMaxRetryDelay,
|
||||||
sseSleepFn,
|
sseSleepFn,
|
||||||
url,
|
url,
|
||||||
...options
|
...options
|
||||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||||
let lastEventId: string | undefined;
|
let lastEventId: string | undefined;
|
||||||
|
|
||||||
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||||
|
|
||||||
const createStream = async function* () {
|
const createStream = async function* () {
|
||||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
const signal = options.signal ?? new AbortController().signal;
|
const signal = options.signal ?? new AbortController().signal;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (signal.aborted) break;
|
if (signal.aborted) break;
|
||||||
|
|
||||||
attempt++;
|
attempt++;
|
||||||
|
|
||||||
const headers =
|
const headers =
|
||||||
options.headers instanceof Headers
|
options.headers instanceof Headers
|
||||||
? options.headers
|
? options.headers
|
||||||
: new Headers(options.headers as Record<string, string> | undefined);
|
: new Headers(options.headers as Record<string, string> | undefined);
|
||||||
|
|
||||||
if (lastEventId !== undefined) {
|
if (lastEventId !== undefined) {
|
||||||
headers.set("Last-Event-ID", lastEventId);
|
headers.set('Last-Event-ID', lastEventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestInit: RequestInit = {
|
const requestInit: RequestInit = {
|
||||||
redirect: "follow",
|
redirect: 'follow',
|
||||||
...options,
|
...options,
|
||||||
body: options.serializedBody,
|
body: options.serializedBody,
|
||||||
headers,
|
headers,
|
||||||
signal,
|
signal,
|
||||||
};
|
};
|
||||||
let request = new Request(url, requestInit);
|
let request = new Request(url, requestInit);
|
||||||
if (onRequest) {
|
if (onRequest) {
|
||||||
request = await onRequest(url, requestInit);
|
request = await onRequest(url, requestInit);
|
||||||
}
|
}
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
// fetch must be assigned here, otherwise it would throw the error:
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
const _fetch = options.fetch ?? globalThis.fetch;
|
const _fetch = options.fetch ?? globalThis.fetch;
|
||||||
const response = await _fetch(request);
|
const response = await _fetch(request);
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
if (!response.body) throw new Error("No body in SSE response");
|
if (!response.body) throw new Error('No body in SSE response');
|
||||||
|
|
||||||
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||||
|
|
||||||
let buffer = "";
|
let buffer = '';
|
||||||
|
|
||||||
const abortHandler = () => {
|
const abortHandler = () => {
|
||||||
try {
|
try {
|
||||||
reader.cancel();
|
reader.cancel();
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
signal.addEventListener("abort", abortHandler);
|
signal.addEventListener('abort', abortHandler);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buffer += value;
|
buffer += value;
|
||||||
// Normalize line endings: CRLF -> LF, then CR -> LF
|
// Normalize line endings: CRLF -> LF, then CR -> LF
|
||||||
buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
|
||||||
const chunks = buffer.split("\n\n");
|
const chunks = buffer.split('\n\n');
|
||||||
buffer = chunks.pop() ?? "";
|
buffer = chunks.pop() ?? '';
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
const lines = chunk.split("\n");
|
const lines = chunk.split('\n');
|
||||||
const dataLines: Array<string> = [];
|
const dataLines: Array<string> = [];
|
||||||
let eventName: string | undefined;
|
let eventName: string | undefined;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith("data:")) {
|
if (line.startsWith('data:')) {
|
||||||
dataLines.push(line.replace(/^data:\s*/, ""));
|
dataLines.push(line.replace(/^data:\s*/, ''));
|
||||||
} else if (line.startsWith("event:")) {
|
} else if (line.startsWith('event:')) {
|
||||||
eventName = line.replace(/^event:\s*/, "");
|
eventName = line.replace(/^event:\s*/, '');
|
||||||
} else if (line.startsWith("id:")) {
|
} else if (line.startsWith('id:')) {
|
||||||
lastEventId = line.replace(/^id:\s*/, "");
|
lastEventId = line.replace(/^id:\s*/, '');
|
||||||
} else if (line.startsWith("retry:")) {
|
} else if (line.startsWith('retry:')) {
|
||||||
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
|
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
retryDelay = parsed;
|
retryDelay = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: unknown;
|
let data: unknown;
|
||||||
let parsedJson = false;
|
let parsedJson = false;
|
||||||
|
|
||||||
if (dataLines.length) {
|
if (dataLines.length) {
|
||||||
const rawData = dataLines.join("\n");
|
const rawData = dataLines.join('\n');
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(rawData);
|
data = JSON.parse(rawData);
|
||||||
parsedJson = true;
|
parsedJson = true;
|
||||||
} catch {
|
} catch {
|
||||||
data = rawData;
|
data = rawData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedJson) {
|
if (parsedJson) {
|
||||||
if (responseValidator) {
|
if (responseValidator) {
|
||||||
await responseValidator(data);
|
await responseValidator(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseTransformer) {
|
if (responseTransformer) {
|
||||||
data = await responseTransformer(data);
|
data = await responseTransformer(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSseEvent?.({
|
onSseEvent?.({
|
||||||
data,
|
data,
|
||||||
event: eventName,
|
event: eventName,
|
||||||
id: lastEventId,
|
id: lastEventId,
|
||||||
retry: retryDelay,
|
retry: retryDelay,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dataLines.length) {
|
if (dataLines.length) {
|
||||||
yield data as any;
|
yield data as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
signal.removeEventListener("abort", abortHandler);
|
signal.removeEventListener('abort', abortHandler);
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
break; // exit loop on normal completion
|
break; // exit loop on normal completion
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// connection failed or aborted; retry after delay
|
// connection failed or aborted; retry after delay
|
||||||
onSseError?.(error);
|
onSseError?.(error);
|
||||||
|
|
||||||
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
|
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
|
||||||
break; // stop after firing error
|
break; // stop after firing error
|
||||||
}
|
}
|
||||||
|
|
||||||
// exponential backoff: double retry each attempt, cap at 30s
|
// exponential backoff: double retry each attempt, cap at 30s
|
||||||
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
|
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
|
||||||
await sleep(backoff);
|
await sleep(backoff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = createStream();
|
const stream = createStream();
|
||||||
|
|
||||||
return { stream };
|
return { stream };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,105 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Auth, AuthToken } from "./auth.gen";
|
import type { Auth, AuthToken } from './auth.gen';
|
||||||
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen";
|
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
|
||||||
|
|
||||||
export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
export type HttpMethod =
|
||||||
|
| 'connect'
|
||||||
|
| 'delete'
|
||||||
|
| 'get'
|
||||||
|
| 'head'
|
||||||
|
| 'options'
|
||||||
|
| 'patch'
|
||||||
|
| 'post'
|
||||||
|
| 'put'
|
||||||
|
| 'trace';
|
||||||
|
|
||||||
export type Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never> = {
|
export type Client<
|
||||||
/**
|
RequestFn = never,
|
||||||
* Returns the final request URL.
|
Config = unknown,
|
||||||
*/
|
MethodFn = never,
|
||||||
buildUrl: BuildUrlFn;
|
BuildUrlFn = never,
|
||||||
getConfig: () => Config;
|
SseFn = never,
|
||||||
request: RequestFn;
|
> = {
|
||||||
setConfig: (config: Config) => Config;
|
/**
|
||||||
|
* Returns the final request URL.
|
||||||
|
*/
|
||||||
|
buildUrl: BuildUrlFn;
|
||||||
|
getConfig: () => Config;
|
||||||
|
request: RequestFn;
|
||||||
|
setConfig: (config: Config) => Config;
|
||||||
} & {
|
} & {
|
||||||
[K in HttpMethod]: MethodFn;
|
[K in HttpMethod]: MethodFn;
|
||||||
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
|
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
/**
|
/**
|
||||||
* Auth token or a function returning auth token. The resolved value will be
|
* Auth token or a function returning auth token. The resolved value will be
|
||||||
* added to the request payload as defined by its `security` array.
|
* added to the request payload as defined by its `security` array.
|
||||||
*/
|
*/
|
||||||
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
||||||
/**
|
/**
|
||||||
* A function for serializing request body parameter. By default,
|
* A function for serializing request body parameter. By default,
|
||||||
* {@link JSON.stringify()} will be used.
|
* {@link JSON.stringify()} will be used.
|
||||||
*/
|
*/
|
||||||
bodySerializer?: BodySerializer | null;
|
bodySerializer?: BodySerializer | null;
|
||||||
/**
|
/**
|
||||||
* An object containing any HTTP headers that you want to pre-populate your
|
* An object containing any HTTP headers that you want to pre-populate your
|
||||||
* `Headers` object with.
|
* `Headers` object with.
|
||||||
*
|
*
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||||
*/
|
*/
|
||||||
headers?:
|
headers?:
|
||||||
| RequestInit["headers"]
|
| RequestInit['headers']
|
||||||
| Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>;
|
| Record<
|
||||||
/**
|
string,
|
||||||
* The request method.
|
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
|
||||||
*
|
>;
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
/**
|
||||||
*/
|
* The request method.
|
||||||
method?: Uppercase<HttpMethod>;
|
*
|
||||||
/**
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||||
* A function for serializing request query parameters. By default, arrays
|
*/
|
||||||
* will be exploded in form style, objects will be exploded in deepObject
|
method?: Uppercase<HttpMethod>;
|
||||||
* style, and reserved characters are percent-encoded.
|
/**
|
||||||
*
|
* A function for serializing request query parameters. By default, arrays
|
||||||
* This method will have no effect if the native `paramsSerializer()` Axios
|
* will be exploded in form style, objects will be exploded in deepObject
|
||||||
* API function is used.
|
* style, and reserved characters are percent-encoded.
|
||||||
*
|
*
|
||||||
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
* This method will have no effect if the native `paramsSerializer()` Axios
|
||||||
*/
|
* API function is used.
|
||||||
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
*
|
||||||
/**
|
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
||||||
* A function validating request data. This is useful if you want to ensure
|
*/
|
||||||
* the request conforms to the desired shape, so it can be safely sent to
|
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
||||||
* the server.
|
/**
|
||||||
*/
|
* A function validating request data. This is useful if you want to ensure
|
||||||
requestValidator?: (data: unknown) => Promise<unknown>;
|
* the request conforms to the desired shape, so it can be safely sent to
|
||||||
/**
|
* the server.
|
||||||
* A function transforming response data before it's returned. This is useful
|
*/
|
||||||
* for post-processing data, e.g. converting ISO strings into Date objects.
|
requestValidator?: (data: unknown) => Promise<unknown>;
|
||||||
*/
|
/**
|
||||||
responseTransformer?: (data: unknown) => Promise<unknown>;
|
* A function transforming response data before it's returned. This is useful
|
||||||
/**
|
* for post-processing data, e.g. converting ISO strings into Date objects.
|
||||||
* A function validating response data. This is useful if you want to ensure
|
*/
|
||||||
* the response conforms to the desired shape, so it can be safely passed to
|
responseTransformer?: (data: unknown) => Promise<unknown>;
|
||||||
* the transformers and returned to the user.
|
/**
|
||||||
*/
|
* A function validating response data. This is useful if you want to ensure
|
||||||
responseValidator?: (data: unknown) => Promise<unknown>;
|
* the response conforms to the desired shape, so it can be safely passed to
|
||||||
|
* the transformers and returned to the user.
|
||||||
|
*/
|
||||||
|
responseValidator?: (data: unknown) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||||
? true
|
? true
|
||||||
: [T] extends [never | undefined]
|
: [T] extends [never | undefined]
|
||||||
? [undefined] extends [T]
|
? [undefined] extends [T]
|
||||||
? false
|
? false
|
||||||
: true
|
: true
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
export type OmitNever<T extends Record<string, unknown>> = {
|
export type OmitNever<T extends Record<string, unknown>> = {
|
||||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
|
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,138 +1,141 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen";
|
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
|
||||||
import {
|
import {
|
||||||
type ArraySeparatorStyle,
|
type ArraySeparatorStyle,
|
||||||
serializeArrayParam,
|
serializeArrayParam,
|
||||||
serializeObjectParam,
|
serializeObjectParam,
|
||||||
serializePrimitiveParam,
|
serializePrimitiveParam,
|
||||||
} from "./pathSerializer.gen";
|
} from './pathSerializer.gen';
|
||||||
|
|
||||||
export interface PathSerializer {
|
export interface PathSerializer {
|
||||||
path: Record<string, unknown>;
|
path: Record<string, unknown>;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
||||||
|
|
||||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||||
let url = _url;
|
let url = _url;
|
||||||
const matches = _url.match(PATH_PARAM_RE);
|
const matches = _url.match(PATH_PARAM_RE);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
let explode = false;
|
let explode = false;
|
||||||
let name = match.substring(1, match.length - 1);
|
let name = match.substring(1, match.length - 1);
|
||||||
let style: ArraySeparatorStyle = "simple";
|
let style: ArraySeparatorStyle = 'simple';
|
||||||
|
|
||||||
if (name.endsWith("*")) {
|
if (name.endsWith('*')) {
|
||||||
explode = true;
|
explode = true;
|
||||||
name = name.substring(0, name.length - 1);
|
name = name.substring(0, name.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.startsWith(".")) {
|
if (name.startsWith('.')) {
|
||||||
name = name.substring(1);
|
name = name.substring(1);
|
||||||
style = "label";
|
style = 'label';
|
||||||
} else if (name.startsWith(";")) {
|
} else if (name.startsWith(';')) {
|
||||||
name = name.substring(1);
|
name = name.substring(1);
|
||||||
style = "matrix";
|
style = 'matrix';
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = path[name];
|
const value = path[name];
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
|
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (typeof value === 'object') {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
serializeObjectParam({
|
serializeObjectParam({
|
||||||
explode,
|
explode,
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
value: value as Record<string, unknown>,
|
value: value as Record<string, unknown>,
|
||||||
valueOnly: true,
|
valueOnly: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style === "matrix") {
|
if (style === 'matrix') {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
`;${serializePrimitiveParam({
|
`;${serializePrimitiveParam({
|
||||||
name,
|
name,
|
||||||
value: value as string,
|
value: value as string,
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string));
|
const replaceValue = encodeURIComponent(
|
||||||
url = url.replace(match, replaceValue);
|
style === 'label' ? `.${value as string}` : (value as string),
|
||||||
}
|
);
|
||||||
}
|
url = url.replace(match, replaceValue);
|
||||||
return url;
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUrl = ({
|
export const getUrl = ({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path,
|
path,
|
||||||
query,
|
query,
|
||||||
querySerializer,
|
querySerializer,
|
||||||
url: _url,
|
url: _url,
|
||||||
}: {
|
}: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
path?: Record<string, unknown>;
|
path?: Record<string, unknown>;
|
||||||
query?: Record<string, unknown>;
|
query?: Record<string, unknown>;
|
||||||
querySerializer: QuerySerializer;
|
querySerializer: QuerySerializer;
|
||||||
url: string;
|
url: string;
|
||||||
}) => {
|
}) => {
|
||||||
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||||
let url = (baseUrl ?? "") + pathUrl;
|
let url = (baseUrl ?? '') + pathUrl;
|
||||||
if (path) {
|
if (path) {
|
||||||
url = defaultPathSerializer({ path, url });
|
url = defaultPathSerializer({ path, url });
|
||||||
}
|
}
|
||||||
let search = query ? querySerializer(query) : "";
|
let search = query ? querySerializer(query) : '';
|
||||||
if (search.startsWith("?")) {
|
if (search.startsWith('?')) {
|
||||||
search = search.substring(1);
|
search = search.substring(1);
|
||||||
}
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
url += `?${search}`;
|
url += `?${search}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getValidRequestBody(options: {
|
export function getValidRequestBody(options: {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
bodySerializer?: BodySerializer | null;
|
bodySerializer?: BodySerializer | null;
|
||||||
serializedBody?: unknown;
|
serializedBody?: unknown;
|
||||||
}) {
|
}) {
|
||||||
const hasBody = options.body !== undefined;
|
const hasBody = options.body !== undefined;
|
||||||
const isSerializedBody = hasBody && options.bodySerializer;
|
const isSerializedBody = hasBody && options.bodySerializer;
|
||||||
|
|
||||||
if (isSerializedBody) {
|
if (isSerializedBody) {
|
||||||
if ("serializedBody" in options) {
|
if ('serializedBody' in options) {
|
||||||
const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "";
|
const hasSerializedBody =
|
||||||
|
options.serializedBody !== undefined && options.serializedBody !== '';
|
||||||
|
|
||||||
return hasSerializedBody ? options.serializedBody : null;
|
return hasSerializedBody ? options.serializedBody : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// not all clients implement a serializedBody property (i.e. client-axios)
|
// not all clients implement a serializedBody property (i.e. client-axios)
|
||||||
return options.body !== "" ? options.body : null;
|
return options.body !== '' ? options.body : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// plain/text body
|
// plain/text body
|
||||||
if (hasBody) {
|
if (hasBody) {
|
||||||
return options.body;
|
return options.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no body was provided
|
// no body was provided
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,22 +13,28 @@ export {
|
||||||
deleteRepository,
|
deleteRepository,
|
||||||
deleteSnapshot,
|
deleteSnapshot,
|
||||||
deleteSnapshots,
|
deleteSnapshots,
|
||||||
|
deleteSsoInvitation,
|
||||||
|
deleteSsoProvider,
|
||||||
|
deleteUserAccount,
|
||||||
deleteVolume,
|
deleteVolume,
|
||||||
devPanelExec,
|
devPanelExec,
|
||||||
downloadResticPassword,
|
downloadResticPassword,
|
||||||
dumpSnapshot,
|
dumpSnapshot,
|
||||||
|
getAdminUsers,
|
||||||
getBackupProgress,
|
getBackupProgress,
|
||||||
getBackupSchedule,
|
getBackupSchedule,
|
||||||
getBackupScheduleForVolume,
|
getBackupScheduleForVolume,
|
||||||
getDevPanel,
|
getDevPanel,
|
||||||
getMirrorCompatibility,
|
getMirrorCompatibility,
|
||||||
getNotificationDestination,
|
getNotificationDestination,
|
||||||
|
getPublicSsoProviders,
|
||||||
getRegistrationStatus,
|
getRegistrationStatus,
|
||||||
getRepository,
|
getRepository,
|
||||||
getRepositoryStats,
|
getRepositoryStats,
|
||||||
getScheduleMirrors,
|
getScheduleMirrors,
|
||||||
getScheduleNotifications,
|
getScheduleNotifications,
|
||||||
getSnapshotDetails,
|
getSnapshotDetails,
|
||||||
|
getSsoSettings,
|
||||||
getStatus,
|
getStatus,
|
||||||
getSystemInfo,
|
getSystemInfo,
|
||||||
getUpdates,
|
getUpdates,
|
||||||
|
|
@ -63,6 +69,7 @@ export {
|
||||||
updateRepository,
|
updateRepository,
|
||||||
updateScheduleMirrors,
|
updateScheduleMirrors,
|
||||||
updateScheduleNotifications,
|
updateScheduleNotifications,
|
||||||
|
updateSsoProviderAutoLinking,
|
||||||
updateVolume,
|
updateVolume,
|
||||||
} from "./sdk.gen";
|
} from "./sdk.gen";
|
||||||
export type {
|
export type {
|
||||||
|
|
@ -102,6 +109,15 @@ export type {
|
||||||
DeleteSnapshotsData,
|
DeleteSnapshotsData,
|
||||||
DeleteSnapshotsResponse,
|
DeleteSnapshotsResponse,
|
||||||
DeleteSnapshotsResponses,
|
DeleteSnapshotsResponses,
|
||||||
|
DeleteSsoInvitationData,
|
||||||
|
DeleteSsoInvitationErrors,
|
||||||
|
DeleteSsoInvitationResponses,
|
||||||
|
DeleteSsoProviderData,
|
||||||
|
DeleteSsoProviderErrors,
|
||||||
|
DeleteSsoProviderResponses,
|
||||||
|
DeleteUserAccountData,
|
||||||
|
DeleteUserAccountErrors,
|
||||||
|
DeleteUserAccountResponses,
|
||||||
DeleteVolumeData,
|
DeleteVolumeData,
|
||||||
DeleteVolumeResponse,
|
DeleteVolumeResponse,
|
||||||
DeleteVolumeResponses,
|
DeleteVolumeResponses,
|
||||||
|
|
@ -115,6 +131,9 @@ export type {
|
||||||
DumpSnapshotData,
|
DumpSnapshotData,
|
||||||
DumpSnapshotResponse,
|
DumpSnapshotResponse,
|
||||||
DumpSnapshotResponses,
|
DumpSnapshotResponses,
|
||||||
|
GetAdminUsersData,
|
||||||
|
GetAdminUsersResponse,
|
||||||
|
GetAdminUsersResponses,
|
||||||
GetBackupProgressData,
|
GetBackupProgressData,
|
||||||
GetBackupProgressResponse,
|
GetBackupProgressResponse,
|
||||||
GetBackupProgressResponses,
|
GetBackupProgressResponses,
|
||||||
|
|
@ -134,6 +153,9 @@ export type {
|
||||||
GetNotificationDestinationErrors,
|
GetNotificationDestinationErrors,
|
||||||
GetNotificationDestinationResponse,
|
GetNotificationDestinationResponse,
|
||||||
GetNotificationDestinationResponses,
|
GetNotificationDestinationResponses,
|
||||||
|
GetPublicSsoProvidersData,
|
||||||
|
GetPublicSsoProvidersResponse,
|
||||||
|
GetPublicSsoProvidersResponses,
|
||||||
GetRegistrationStatusData,
|
GetRegistrationStatusData,
|
||||||
GetRegistrationStatusResponse,
|
GetRegistrationStatusResponse,
|
||||||
GetRegistrationStatusResponses,
|
GetRegistrationStatusResponses,
|
||||||
|
|
@ -152,6 +174,9 @@ export type {
|
||||||
GetSnapshotDetailsData,
|
GetSnapshotDetailsData,
|
||||||
GetSnapshotDetailsResponse,
|
GetSnapshotDetailsResponse,
|
||||||
GetSnapshotDetailsResponses,
|
GetSnapshotDetailsResponses,
|
||||||
|
GetSsoSettingsData,
|
||||||
|
GetSsoSettingsResponse,
|
||||||
|
GetSsoSettingsResponses,
|
||||||
GetStatusData,
|
GetStatusData,
|
||||||
GetStatusResponse,
|
GetStatusResponse,
|
||||||
GetStatusResponses,
|
GetStatusResponses,
|
||||||
|
|
@ -258,6 +283,9 @@ export type {
|
||||||
UpdateScheduleNotificationsData,
|
UpdateScheduleNotificationsData,
|
||||||
UpdateScheduleNotificationsResponse,
|
UpdateScheduleNotificationsResponse,
|
||||||
UpdateScheduleNotificationsResponses,
|
UpdateScheduleNotificationsResponses,
|
||||||
|
UpdateSsoProviderAutoLinkingData,
|
||||||
|
UpdateSsoProviderAutoLinkingErrors,
|
||||||
|
UpdateSsoProviderAutoLinkingResponses,
|
||||||
UpdateVolumeData,
|
UpdateVolumeData,
|
||||||
UpdateVolumeErrors,
|
UpdateVolumeErrors,
|
||||||
UpdateVolumeResponse,
|
UpdateVolumeResponse,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -18,6 +18,7 @@ import { cn } from "~/client/lib/utils";
|
||||||
import { APP_VERSION, RCLONE_VERSION, RESTIC_VERSION, SHOUTRRR_VERSION } from "~/client/lib/version";
|
import { APP_VERSION, RCLONE_VERSION, RESTIC_VERSION, SHOUTRRR_VERSION } from "~/client/lib/version";
|
||||||
import { useUpdates } from "~/client/hooks/use-updates";
|
import { useUpdates } from "~/client/hooks/use-updates";
|
||||||
import { ReleaseNotesDialog } from "./release-notes-dialog";
|
import { ReleaseNotesDialog } from "./release-notes-dialog";
|
||||||
|
import { OrganizationSwitcher } from "./organization-switcher";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
|
|
@ -131,7 +132,8 @@ export function AppSidebar() {
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter className="p-4 border-r border-t border-border/50">
|
<SidebarFooter className="p-4 border-r border-border/50">
|
||||||
|
<OrganizationSwitcher />
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<HoverCard openDelay={200}>
|
<HoverCard openDelay={200}>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function GridBackground({ children, className, containerClassName }: Grid
|
||||||
"bg-[size:40px_40px]",
|
"bg-[size:40px_40px]",
|
||||||
"bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
"bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
||||||
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||||
"[mask-image:radial-gradient(ellipse_at_top,black_40%,transparent_100%)]",
|
"[mask-image:radial-gradient(ellipse_at_top,black_70%,transparent_100%)]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className={cn("relative container m-auto z-10", className)}>{children}</div>
|
<div className={cn("relative container m-auto z-10", className)}>{children}</div>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export function Layout({ loaderData }: Props) {
|
||||||
{loaderData.user && (
|
{loaderData.user && (
|
||||||
<div className="flex items-center bg-muted/30 border border-border/50 px-2 py-1 rounded-full shadow-sm">
|
<div className="flex items-center bg-muted/30 border border-border/50 px-2 py-1 rounded-full shadow-sm">
|
||||||
<span className="text-sm text-muted-foreground hidden md:inline-flex pl-2 mr-5">
|
<span className="text-sm text-muted-foreground hidden md:inline-flex pl-2 mr-5">
|
||||||
<span className="text-foreground">{loaderData.user?.username}</span>
|
<span className="text-foreground">{loaderData.user.name}</span>
|
||||||
</span>
|
</span>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
|
||||||
110
app/client/components/organization-switcher.tsx
Normal file
110
app/client/components/organization-switcher.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { ChevronsUpDown } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/client/components/ui/dropdown-menu";
|
||||||
|
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "~/client/components/ui/sidebar";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useOrganizationContext } from "../hooks/use-org-context";
|
||||||
|
|
||||||
|
function getOrganizationInitials(name?: string): string {
|
||||||
|
const trimmedName = name?.trim();
|
||||||
|
|
||||||
|
if (!trimmedName) {
|
||||||
|
return "O";
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedName
|
||||||
|
.split(/\s+/)
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase())
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrganizationSwitcher() {
|
||||||
|
const { isMobile } = useSidebar();
|
||||||
|
const { organizations, activeOrganization } = useOrganizationContext();
|
||||||
|
|
||||||
|
const switchOrganizationMutation = useMutation({
|
||||||
|
mutationFn: async (organizationId: string) => {
|
||||||
|
const { error } = await authClient.organization.setActive({ organizationId });
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
const message = error instanceof Error ? error.message : "Unexpected error while switching organizations";
|
||||||
|
toast.error("Failed to switch organization", { description: message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organizations === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizations.length <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenu className="mb-3">
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<div className="bg-black text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg">
|
||||||
|
{activeOrganization?.logo ? (
|
||||||
|
<img
|
||||||
|
src={activeOrganization.logo}
|
||||||
|
alt={`${activeOrganization.name} logo`}
|
||||||
|
className="size-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-semibold">{getOrganizationInitials(activeOrganization?.name)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span className="truncate font-medium">{activeOrganization?.name}</span>
|
||||||
|
<span className="truncate text-xs">{organizations.length} organizations</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-auto group-data-[collapsible=icon]:hidden" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||||
|
align="start"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="text-muted-foreground text-xs">Organizations</DropdownMenuLabel>
|
||||||
|
{organizations.map((organization) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={organization.id}
|
||||||
|
onClick={() => switchOrganizationMutation.mutate(organization.id)}
|
||||||
|
className="gap-2 p-2"
|
||||||
|
disabled={switchOrganizationMutation.isPending}
|
||||||
|
>
|
||||||
|
<div className="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-md border">
|
||||||
|
{organization.logo ? (
|
||||||
|
<img src={organization.logo} alt={`${organization.name} logo`} className="size-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<span className="text-[10px] font-semibold">{getOrganizationInitials(organization.name)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="min-w-0 flex-1 truncate">{organization.name}</span>
|
||||||
|
<DropdownMenuShortcut>{organization.id === activeOrganization?.id && "Current"}</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ function Card({ className, children, interactive, ...props }: React.ComponentPro
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground group relative flex flex-col gap-6 border border-border/80 py-6 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.02)] transition-colors duration-300 ",
|
"bg-card text-card-foreground group relative flex flex-col gap-6 border border-border py-6 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.02)] transition-colors duration-300 ",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
217
app/client/components/ui/dropdown-menu.tsx
Normal file
217
app/client/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
|
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
};
|
||||||
6
app/client/functions/get-origin.ts
Normal file
6
app/client/functions/get-origin.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { createIsomorphicFn } from "@tanstack/react-start";
|
||||||
|
import { config } from "~/server/core/config";
|
||||||
|
|
||||||
|
export const getOrigin = createIsomorphicFn()
|
||||||
|
.server(() => config.baseUrl)
|
||||||
|
.client(() => window.location.origin);
|
||||||
13
app/client/hooks/use-org-context.ts
Normal file
13
app/client/hooks/use-org-context.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { useServerFn } from "@tanstack/react-start";
|
||||||
|
import { getOrganizationContext } from "~/server/lib/functions/organization-context";
|
||||||
|
|
||||||
|
export function useOrganizationContext() {
|
||||||
|
const getOrgContext = useServerFn(getOrganizationContext);
|
||||||
|
const { data } = useSuspenseQuery({
|
||||||
|
queryKey: ["organization-context"],
|
||||||
|
queryFn: getOrgContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
organizationClient,
|
organizationClient,
|
||||||
inferAdditionalFields,
|
inferAdditionalFields,
|
||||||
} from "better-auth/client/plugins";
|
} from "better-auth/client/plugins";
|
||||||
|
import { ssoClient } from "@better-auth/sso/client";
|
||||||
import type { auth } from "~/server/lib/auth";
|
import type { auth } from "~/server/lib/auth";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
|
|
@ -14,6 +15,7 @@ export const authClient = createAuthClient({
|
||||||
usernameClient(),
|
usernameClient(),
|
||||||
adminClient(),
|
adminClient(),
|
||||||
organizationClient(),
|
organizationClient(),
|
||||||
|
ssoClient(),
|
||||||
twoFactorClient(),
|
twoFactorClient(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
62
app/client/lib/auth-errors.ts
Normal file
62
app/client/lib/auth-errors.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
export type LoginErrorCode =
|
||||||
|
| "ACCOUNT_LINK_REQUIRED"
|
||||||
|
| "EMAIL_NOT_VERIFIED"
|
||||||
|
| "INVITE_REQUIRED"
|
||||||
|
| "BANNED_USER"
|
||||||
|
| "SSO_LOGIN_FAILED";
|
||||||
|
|
||||||
|
export function decodeLoginError(error?: string): LoginErrorCode | null {
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
decoded = decodeURIComponent(error);
|
||||||
|
} catch {
|
||||||
|
decoded = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded = decoded.toLowerCase().replace(/[-_\s]+/g, "_");
|
||||||
|
|
||||||
|
if (decoded.includes("account_not_linked")) {
|
||||||
|
return "ACCOUNT_LINK_REQUIRED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.includes("email_not_verified")) {
|
||||||
|
return "EMAIL_NOT_VERIFIED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.includes("banned_user") || decoded.includes("banned")) {
|
||||||
|
return "BANNED_USER";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
decoded.includes("access_denied") ||
|
||||||
|
decoded.includes("must_be_invited") ||
|
||||||
|
decoded.includes("unable_to_create_session") ||
|
||||||
|
decoded.includes("invite")
|
||||||
|
) {
|
||||||
|
return "INVITE_REQUIRED";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "SSO_LOGIN_FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLoginErrorDescription(errorCode: LoginErrorCode | null): string | null {
|
||||||
|
switch (errorCode) {
|
||||||
|
case "ACCOUNT_LINK_REQUIRED":
|
||||||
|
return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator.";
|
||||||
|
case "EMAIL_NOT_VERIFIED":
|
||||||
|
return "Your identity provider did not mark your email as verified.";
|
||||||
|
case "INVITE_REQUIRED":
|
||||||
|
return "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO.";
|
||||||
|
case "BANNED_USER":
|
||||||
|
return "You have been banned from this application. Please contact support if you believe this is an error.";
|
||||||
|
case "SSO_LOGIN_FAILED":
|
||||||
|
return "SSO authentication failed. Please try again.";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/client/modules/auth/routes/__tests__/login.test.tsx
Normal file
57
app/client/modules/auth/routes/__tests__/login.test.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
await mock.module("@tanstack/react-router", () => ({
|
||||||
|
useNavigate: () => mock(() => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await mock.module("~/client/api-client/@tanstack/react-query.gen", () => ({
|
||||||
|
getPublicSsoProvidersOptions: () => ({
|
||||||
|
queryKey: ["public-sso-providers"],
|
||||||
|
queryFn: async () => ({ providers: [] }),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await mock.module("~/client/lib/auth-client", () => ({
|
||||||
|
authClient: {
|
||||||
|
getSession: mock(async () => ({ data: null })),
|
||||||
|
signIn: {
|
||||||
|
username: mock(async () => ({ data: null, error: null })),
|
||||||
|
sso: mock(async () => ({ data: null, error: null })),
|
||||||
|
},
|
||||||
|
twoFactor: {
|
||||||
|
verifyTotp: mock(async () => ({ data: null, error: null })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { LoginPage } from "../login";
|
||||||
|
|
||||||
|
const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
const inviteOnlyMessage =
|
||||||
|
"Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO.";
|
||||||
|
|
||||||
|
describe("LoginPage", () => {
|
||||||
|
test("shows an invite-only message when SSO returns access_denied", async () => {
|
||||||
|
const queryClient = createTestQueryClient();
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<LoginPage error="access_denied" />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByText(inviteOnlyMessage)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows an invite-only message for URL-encoded invitation errors", async () => {
|
||||||
|
const queryClient = createTestQueryClient();
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<LoginPage error="must%20be%20invited" />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByText(inviteOnlyMessage)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -10,9 +10,13 @@ import { Input } from "~/client/components/ui/input";
|
||||||
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp";
|
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp";
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/auth-errors";
|
||||||
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { normalizeUsername } from "~/lib/username";
|
import { normalizeUsername } from "~/lib/username";
|
||||||
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { getPublicSsoProvidersOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
const loginSchema = type({
|
const loginSchema = type({
|
||||||
username: "2<=string<=50",
|
username: "2<=string<=50",
|
||||||
|
|
@ -21,7 +25,11 @@ const loginSchema = type({
|
||||||
|
|
||||||
type LoginFormValues = typeof loginSchema.inferIn;
|
type LoginFormValues = typeof loginSchema.inferIn;
|
||||||
|
|
||||||
export function LoginPage() {
|
type LoginPageProps = {
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showResetDialog, setShowResetDialog] = useState(false);
|
const [showResetDialog, setShowResetDialog] = useState(false);
|
||||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
|
@ -29,6 +37,12 @@ export function LoginPage() {
|
||||||
const [totpCode, setTotpCode] = useState("");
|
const [totpCode, setTotpCode] = useState("");
|
||||||
const [isVerifying2FA, setIsVerifying2FA] = useState(false);
|
const [isVerifying2FA, setIsVerifying2FA] = useState(false);
|
||||||
const [trustDevice, setTrustDevice] = useState(false);
|
const [trustDevice, setTrustDevice] = useState(false);
|
||||||
|
const errorCode = decodeLoginError(error);
|
||||||
|
const errorDescription = getLoginErrorDescription(errorCode);
|
||||||
|
|
||||||
|
const { data: ssoProviders } = useSuspenseQuery({
|
||||||
|
...getPublicSsoProvidersOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<LoginFormValues>({
|
const form = useForm<LoginFormValues>({
|
||||||
resolver: arktypeResolver(loginSchema),
|
resolver: arktypeResolver(loginSchema),
|
||||||
|
|
@ -115,6 +129,27 @@ export function LoginPage() {
|
||||||
form.reset();
|
form.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ssoLoginMutation = useMutation({
|
||||||
|
mutationFn: async (providerId: string) => {
|
||||||
|
const callbackPath = "/login";
|
||||||
|
const { data, error } = await authClient.signIn.sso({
|
||||||
|
providerId: providerId,
|
||||||
|
callbackURL: callbackPath,
|
||||||
|
errorCallbackURL: callbackPath,
|
||||||
|
});
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
window.location.href = data.url;
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("SSO Login failed", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (requires2FA) {
|
if (requires2FA) {
|
||||||
return (
|
return (
|
||||||
<AuthLayout title="Two-Factor Authentication" description="Enter the 6-digit code from your authenticator app">
|
<AuthLayout title="Two-Factor Authentication" description="Enter the 6-digit code from your authenticator app">
|
||||||
|
|
@ -186,6 +221,9 @@ export function LoginPage() {
|
||||||
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
|
<AuthLayout title="Login to your account" description="Enter your credentials below to login to your account">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div className={cn("rounded-md border border-destructive/50 p-3 text-sm", { hidden: !errorDescription })}>
|
||||||
|
{errorDescription}
|
||||||
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
|
|
@ -227,6 +265,27 @@ export function LoginPage() {
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
{ssoProviders.providers.length > 0 && (
|
||||||
|
<div className="pt-4 border-t border-border/60 space-y-3">
|
||||||
|
<p className="text-sm font-medium">Alternative Sign-in</p>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{ssoProviders.providers.map((provider) => (
|
||||||
|
<Button
|
||||||
|
key={provider.providerId}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
loading={ssoLoginMutation.isPending}
|
||||||
|
disabled={ssoLoginMutation.isPending}
|
||||||
|
onClick={() => ssoLoginMutation.mutate(provider.providerId)}
|
||||||
|
>
|
||||||
|
Log in with {provider.providerId}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ResetPasswordDialog open={showResetDialog} onOpenChange={setShowResetDialog} />
|
<ResetPasswordDialog open={showResetDialog} onOpenChange={setShowResetDialog} />
|
||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,366 @@
|
||||||
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { Ban, Trash2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
deleteSsoInvitationMutation,
|
||||||
|
deleteSsoProviderMutation,
|
||||||
|
getSsoSettingsOptions,
|
||||||
|
updateSsoProviderAutoLinkingMutation,
|
||||||
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "~/client/components/ui/alert-dialog";
|
||||||
|
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { Label } from "~/client/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
|
import { Switch } from "~/client/components/ui/switch";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||||
|
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||||
|
import { formatDateWithMonth } from "~/client/lib/datetime";
|
||||||
|
import { getOrigin } from "~/client/functions/get-origin";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
|
type InvitationRole = "member" | "admin" | "owner";
|
||||||
|
|
||||||
|
export function SsoSettingsSection() {
|
||||||
|
const origin = getOrigin();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { activeOrganization } = useOrganizationContext();
|
||||||
|
const [inviteEmail, setInviteEmail] = useState("");
|
||||||
|
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
||||||
|
|
||||||
|
const { data } = useSuspenseQuery({
|
||||||
|
...getSsoSettingsOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const providers = data.providers;
|
||||||
|
const invitations = data.invitations;
|
||||||
|
|
||||||
|
const updateProviderAutoLinkingMutation = useMutation({
|
||||||
|
...updateSsoProviderAutoLinkingMutation(),
|
||||||
|
onSuccess: (_, v) => {
|
||||||
|
toast.success(v.body?.enabled ? "Automatic account linking enabled" : "Automatic account linking disabled");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to update provider", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteProviderMutation = useMutation({
|
||||||
|
...deleteSsoProviderMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("SSO provider deleted");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to delete provider", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const inviteMemberMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!activeOrganization) {
|
||||||
|
throw new Error("No active organization found in session");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedEmail = inviteEmail.trim().toLowerCase();
|
||||||
|
if (!normalizedEmail) {
|
||||||
|
throw new Error("Email is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await authClient.organization.inviteMember({
|
||||||
|
email: normalizedEmail,
|
||||||
|
role: inviteRole,
|
||||||
|
organizationId: activeOrganization.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Invitation created");
|
||||||
|
setInviteEmail("");
|
||||||
|
setInviteRole("member");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to create invitation", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancelInvitationMutation = useMutation({
|
||||||
|
mutationFn: async (invitationId: string) => {
|
||||||
|
const { error } = await authClient.organization.cancelInvitation({ invitationId });
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Invitation cancelled");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to cancel invitation", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteInvitationMutation = useMutation({
|
||||||
|
...deleteSsoInvitationMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Invitation deleted");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to delete invitation", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">Registered providers</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Manage identity providers used for organization sign-in.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!activeOrganization}
|
||||||
|
onClick={() => void navigate({ to: "/settings/sso/new" })}
|
||||||
|
>
|
||||||
|
Register new
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
Only enable automatic account linking for identity providers you trust. You can change this per provider at
|
||||||
|
any time.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Provider ID</TableHead>
|
||||||
|
<TableHead>Domain</TableHead>
|
||||||
|
<TableHead>Issuer</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Auto-link existing account</TableHead>
|
||||||
|
<TableHead>Callback URL</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{providers.map((provider) => (
|
||||||
|
<TableRow key={provider.providerId}>
|
||||||
|
<TableCell className="font-medium">{provider.providerId}</TableCell>
|
||||||
|
<TableCell>{provider.domain}</TableCell>
|
||||||
|
<TableCell className="break-all">{provider.issuer}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="uppercase text-xs font-medium px-2 py-0.5 rounded border">{provider.type}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={provider.autoLinkMatchingEmails}
|
||||||
|
disabled={updateProviderAutoLinkingMutation.isPending}
|
||||||
|
onCheckedChange={(enabled) => {
|
||||||
|
updateProviderAutoLinkingMutation.mutate({
|
||||||
|
path: { providerId: provider.providerId },
|
||||||
|
body: { enabled },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{provider.autoLinkMatchingEmails ? "On" : "Off"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={`${origin}/api/auth/sso/callback/${provider.providerId}`}
|
||||||
|
className="h-8 max-w-62.5 font-mono text-xs text-muted-foreground"
|
||||||
|
onClick={(e) => e.currentTarget.select()}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Delete provider"
|
||||||
|
loading={deleteProviderMutation.isPending}
|
||||||
|
disabled={deleteProviderMutation.isPending}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete SSO provider</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete the SSO provider <strong>{provider.providerId}</strong>?
|
||||||
|
This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
onClick={() => deleteProviderMutation.mutate({ path: { providerId: provider.providerId } })}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
<TableRow className={cn({ hidden: providers.length > 0 })}>
|
||||||
|
<TableCell colSpan={7} className="text-center text-sm text-muted-foreground">
|
||||||
|
No providers registered yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 border-t border-border/50 pt-6">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-sm font-medium">Invite-only access</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Users must be invited or already have an account before they can sign in using SSO.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 @md:grid-cols-[minmax(0,1fr)_180px_auto]">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="invite-email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="invite-email"
|
||||||
|
type="email"
|
||||||
|
value={inviteEmail}
|
||||||
|
onChange={(event) => setInviteEmail(event.target.value)}
|
||||||
|
placeholder="teammate@example.com"
|
||||||
|
disabled={!activeOrganization || inviteMemberMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="invite-role">Role</Label>
|
||||||
|
<Select
|
||||||
|
value={inviteRole}
|
||||||
|
onValueChange={(value) => setInviteRole(value as InvitationRole)}
|
||||||
|
disabled={!activeOrganization || inviteMemberMutation.isPending}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="invite-role">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
<SelectItem value="owner">Owner</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
loading={inviteMemberMutation.isPending}
|
||||||
|
onClick={() => inviteMemberMutation.mutate()}
|
||||||
|
disabled={!activeOrganization}
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Role</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Expires</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{invitations.map((invitation) => (
|
||||||
|
<TableRow key={invitation.id}>
|
||||||
|
<TableCell className="font-medium">{invitation.email}</TableCell>
|
||||||
|
<TableCell className="uppercase">{invitation.role}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span
|
||||||
|
className={cn(`text-xs font-medium px-2 py-0.5 rounded border`, {
|
||||||
|
"bg-primary/10 border-primary/20": invitation.status === "pending",
|
||||||
|
"bg-muted border-muted-foreground/20": invitation.status !== "pending",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{invitation.status}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatDateWithMonth(invitation.expiresAt)}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Cancel invitation"
|
||||||
|
loading={cancelInvitationMutation.isPending}
|
||||||
|
disabled={cancelInvitationMutation.isPending}
|
||||||
|
onClick={() => cancelInvitationMutation.mutate(invitation.id)}
|
||||||
|
className={cn({ hidden: invitation.status !== "pending" })}
|
||||||
|
>
|
||||||
|
<Ban className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Delete invitation"
|
||||||
|
loading={deleteInvitationMutation.isPending}
|
||||||
|
disabled={deleteInvitationMutation.isPending}
|
||||||
|
onClick={() => deleteInvitationMutation.mutate({ path: { invitationId: invitation.id } })}
|
||||||
|
className={cn({ hidden: invitation.status === "pending" })}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
<TableRow className={cn({ hidden: invitations.length > 0 })}>
|
||||||
|
<TableCell colSpan={5} className="text-center text-sm text-muted-foreground">
|
||||||
|
No invitations yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Shield, ShieldAlert, UserMinus, UserCheck, Trash2, Search, AlertTriangle } from "lucide-react";
|
import { Shield, ShieldAlert, UserCheck, Trash2, Search, AlertTriangle, Ban, KeyRound } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
|
@ -17,29 +17,28 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/client/components/ui/dialog";
|
} from "~/client/components/ui/dialog";
|
||||||
import { CreateUserDialog } from "./create-user-dialog";
|
import { CreateUserDialog } from "./create-user-dialog";
|
||||||
import { getUserDeletionImpactOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import {
|
||||||
|
getAdminUsersOptions,
|
||||||
export function UserManagement() {
|
getUserDeletionImpactOptions,
|
||||||
const { data: session } = authClient.useSession();
|
deleteUserAccountMutation,
|
||||||
const currentUser = session?.user;
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
export function UserManagement({ currentUser }: { currentUser: { id: string } | undefined | null }) {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [userToDelete, setUserToDelete] = useState<string | null>(null);
|
const [userToDelete, setUserToDelete] = useState<string | null>(null);
|
||||||
const [userToBan, setUserToBan] = useState<{ id: string; name: string; isBanned: boolean } | null>(null);
|
const [userToBan, setUserToBan] = useState<{ id: string; name: string; isBanned: boolean } | null>(null);
|
||||||
|
const [userToManageAccounts, setUserToManageAccounts] = useState<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
accounts: { id: string; providerId: string }[];
|
||||||
|
}>();
|
||||||
|
|
||||||
const { data: deletionImpact, isLoading: isLoadingImpact } = useQuery({
|
const { data: deletionImpact, isLoading: isLoadingImpact } = useQuery({
|
||||||
...getUserDeletionImpactOptions({ path: { userId: userToDelete ?? "" } }),
|
...getUserDeletionImpactOptions({ path: { userId: userToDelete ?? "" } }),
|
||||||
enabled: Boolean(userToDelete),
|
enabled: Boolean(userToDelete),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useQuery({
|
const { data } = useSuspenseQuery({ ...getAdminUsersOptions() });
|
||||||
queryKey: ["admin-users"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data, error } = await authClient.admin.listUsers({ query: { limit: 100 } });
|
|
||||||
if (error) throw error;
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setRoleMutation = useMutation({
|
const setRoleMutation = useMutation({
|
||||||
mutationFn: async ({ userId, role }: { userId: string; role: "user" | "admin" }) => {
|
mutationFn: async ({ userId, role }: { userId: string; role: "user" | "admin" }) => {
|
||||||
|
|
@ -48,10 +47,9 @@ export function UserManagement() {
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("User role updated successfully");
|
toast.success("User role updated successfully");
|
||||||
void refetch();
|
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to update role", { description: err.message });
|
toast.error("Failed to update role", { description: error.message });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -62,36 +60,53 @@ export function UserManagement() {
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("User ban status updated successfully");
|
toast.success("User ban status updated successfully");
|
||||||
void refetch();
|
|
||||||
},
|
},
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setUserToBan(null);
|
setUserToBan(null);
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to update ban status", { description: err.message });
|
toast.error("Failed to update ban status", { description: error.message });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredUsers = data?.users.filter(
|
const deleteUser = useMutation({
|
||||||
(user) =>
|
mutationFn: async (userId: string) => {
|
||||||
user.name.toLowerCase().includes(search.toLowerCase()) ||
|
const { error } = await authClient.admin.removeUser({ userId });
|
||||||
user.email.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(user as any).username?.toLowerCase().includes(search.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeleteUser = async () => {
|
|
||||||
if (!userToDelete) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { error } = await authClient.admin.removeUser({ userId: userToDelete });
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
toast.success("User deleted successfully");
|
toast.success("User deleted successfully");
|
||||||
setUserToDelete(null);
|
setUserToDelete(null);
|
||||||
void refetch();
|
},
|
||||||
} catch (err: any) {
|
onError: (error) => {
|
||||||
toast.error("Failed to delete user", { description: err.message });
|
toast.error("Failed to delete user", { description: error.message });
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const deleteAccount = useMutation({
|
||||||
|
...deleteUserAccountMutation(),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
toast.success("Account removed successfully");
|
||||||
|
setUserToManageAccounts((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
accounts: prev.accounts.filter((a) => a.id !== variables.path.accountId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to remove account", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedSearch = search.toLowerCase();
|
||||||
|
const filteredUsers = data.users.filter((user) => {
|
||||||
|
const name = user.name?.toLowerCase() ?? "";
|
||||||
|
const email = user.email.toLowerCase();
|
||||||
|
|
||||||
|
return name.includes(normalizedSearch) || email.includes(normalizedSearch);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-6">
|
<div className="space-y-4 p-6">
|
||||||
|
|
@ -105,7 +120,7 @@ export function UserManagement() {
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CreateUserDialog onUserCreated={() => void refetch()} />
|
<CreateUserDialog />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
|
|
@ -119,21 +134,16 @@ export function UserManagement() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow className={cn({ hidden: !isLoading })}>
|
<TableRow className={cn({ hidden: filteredUsers.length > 0 })}>
|
||||||
<TableCell colSpan={4} className="h-24 text-center">
|
|
||||||
Loading users...
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow className={cn({ hidden: isLoading || (filteredUsers && filteredUsers.length > 0) })}>
|
|
||||||
<TableCell colSpan={4} className="h-24 text-center">
|
<TableCell colSpan={4} className="h-24 text-center">
|
||||||
No users found.
|
No users found.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{filteredUsers?.map((user) => (
|
{filteredUsers.map((user) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow key={user.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">{user.name}</span>
|
<span className="font-medium">{user.name ?? user.email}</span>
|
||||||
<span className="text-sm text-muted-foreground">{user.email}</span>
|
<span className="text-sm text-muted-foreground">{user.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -149,12 +159,12 @@ export function UserManagement() {
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className={cn("flex justify-end gap-2", { hidden: user.id === currentUser?.id })}>
|
<div className={cn("flex justify-end gap-2")}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
title="Demote to User"
|
title="Demote to User"
|
||||||
className={cn({ hidden: user.role !== "admin" })}
|
className={cn({ hidden: user.role !== "admin" || user.id === currentUser?.id })}
|
||||||
onClick={() => setRoleMutation.mutate({ userId: user.id, role: "user" })}
|
onClick={() => setRoleMutation.mutate({ userId: user.id, role: "user" })}
|
||||||
>
|
>
|
||||||
<ShieldAlert className="h-4 w-4" />
|
<ShieldAlert className="h-4 w-4" />
|
||||||
|
|
@ -163,7 +173,7 @@ export function UserManagement() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
title="Promote to Admin"
|
title="Promote to Admin"
|
||||||
className={cn({ hidden: user.role === "admin" })}
|
className={cn({ hidden: user.role === "admin" || user.id === currentUser?.id })}
|
||||||
onClick={() => setRoleMutation.mutate({ userId: user.id, role: "admin" })}
|
onClick={() => setRoleMutation.mutate({ userId: user.id, role: "admin" })}
|
||||||
>
|
>
|
||||||
<Shield className="h-4 w-4" />
|
<Shield className="h-4 w-4" />
|
||||||
|
|
@ -172,25 +182,46 @@ export function UserManagement() {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
title="Unban User"
|
title="Unban user"
|
||||||
className={cn({ hidden: !user.banned })}
|
className={cn({ hidden: !user.banned || user.id === currentUser?.id })}
|
||||||
onClick={() => setUserToBan({ id: user.id, name: user.name, isBanned: true })}
|
onClick={() => setUserToBan({ id: user.id, name: user.name ?? user.email, isBanned: true })}
|
||||||
>
|
>
|
||||||
<UserCheck className="h-4 w-4" />
|
<UserCheck className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
title="Ban User"
|
title="Ban user"
|
||||||
className={cn({ hidden: !!user.banned })}
|
className={cn({ hidden: !!user.banned || user.id === currentUser?.id })}
|
||||||
onClick={() => setUserToBan({ id: user.id, name: user.name, isBanned: false })}
|
onClick={() => setUserToBan({ id: user.id, name: user.name ?? user.email, isBanned: false })}
|
||||||
>
|
>
|
||||||
<UserMinus className="h-4 w-4" />
|
<Ban className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="ghost" size="icon" title="Delete User" onClick={() => setUserToDelete(user.id)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Delete User"
|
||||||
|
className={cn({ hidden: user.id === currentUser?.id })}
|
||||||
|
onClick={() => setUserToDelete(user.id)}
|
||||||
|
>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Manage Accounts"
|
||||||
|
onClick={() =>
|
||||||
|
setUserToManageAccounts({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name ?? user.email,
|
||||||
|
accounts: user.accounts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<KeyRound className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -242,7 +273,11 @@ export function UserManagement() {
|
||||||
<Button variant="outline" onClick={() => setUserToDelete(null)}>
|
<Button variant="outline" onClick={() => setUserToDelete(null)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" disabled={isLoadingImpact} onClick={handleDeleteUser}>
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isLoadingImpact || deleteUser.isPending}
|
||||||
|
onClick={() => deleteUser.mutate(userToDelete!)}
|
||||||
|
>
|
||||||
Delete User
|
Delete User
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|
@ -267,20 +302,60 @@ export function UserManagement() {
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
className={cn({ hidden: !userToBan?.isBanned })}
|
className={cn({ hidden: !userToBan?.isBanned })}
|
||||||
onClick={() => toggleBanUserMutation.mutate({ userId: userToBan!.id, ban: false })}
|
onClick={() => toggleBanUserMutation.mutate({ userId: userToBan?.id ?? "", ban: false })}
|
||||||
>
|
>
|
||||||
Unban User
|
Unban User
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className={cn({ hidden: !!userToBan?.isBanned })}
|
className={cn({ hidden: Boolean(userToBan?.isBanned) })}
|
||||||
onClick={() => toggleBanUserMutation.mutate({ userId: userToBan!.id, ban: true })}
|
onClick={() => toggleBanUserMutation.mutate({ userId: userToBan?.id ?? "", ban: true })}
|
||||||
>
|
>
|
||||||
Ban User
|
Ban User
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={Boolean(userToManageAccounts)} onOpenChange={(open) => !open && setUserToManageAccounts(undefined)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Manage Accounts</DialogTitle>
|
||||||
|
<DialogDescription>Linked authentication accounts for {userToManageAccounts?.name}.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p
|
||||||
|
className={cn("text-sm text-muted-foreground text-center py-4", {
|
||||||
|
hidden: userToManageAccounts?.accounts.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
No accounts linked.
|
||||||
|
</p>
|
||||||
|
{userToManageAccounts?.accounts.map((account) => (
|
||||||
|
<div key={account.id} className="flex items-center justify-between p-3 border rounded-md">
|
||||||
|
<span className="text-sm font-medium">{account.providerId}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Remove account"
|
||||||
|
disabled={deleteAccount.isPending}
|
||||||
|
onClick={() =>
|
||||||
|
deleteAccount.mutate({ path: { userId: userToManageAccounts.id, accountId: account.id } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setUserToManageAccounts(undefined)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
258
app/client/modules/settings/routes/create-sso-provider.tsx
Normal file
258
app/client/modules/settings/routes/create-sso-provider.tsx
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { type } from "arktype";
|
||||||
|
import { ShieldCheck, Plus } from "lucide-react";
|
||||||
|
import { useId } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { updateSsoProviderAutoLinkingMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "~/client/components/ui/form";
|
||||||
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { Switch } from "~/client/components/ui/switch";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { parseError } from "~/client/lib/errors";
|
||||||
|
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||||
|
|
||||||
|
const ssoProviderSchema = type({
|
||||||
|
providerId: "string>=1",
|
||||||
|
issuer: "string>=1",
|
||||||
|
domain: "string>=1",
|
||||||
|
clientId: "string>=1",
|
||||||
|
clientSecret: "string>=1",
|
||||||
|
discoveryEndpoint: "string>=1",
|
||||||
|
linkMatchingEmails: "boolean",
|
||||||
|
});
|
||||||
|
|
||||||
|
type ProviderForm = typeof ssoProviderSchema.infer;
|
||||||
|
|
||||||
|
export function CreateSsoProviderPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const formId = useId();
|
||||||
|
const { activeOrganization } = useOrganizationContext();
|
||||||
|
|
||||||
|
const form = useForm<ProviderForm>({
|
||||||
|
resolver: arktypeResolver(ssoProviderSchema),
|
||||||
|
defaultValues: {
|
||||||
|
providerId: "",
|
||||||
|
issuer: "",
|
||||||
|
domain: "",
|
||||||
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
discoveryEndpoint: "",
|
||||||
|
linkMatchingEmails: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateProviderAutoLinking = useMutation({
|
||||||
|
...updateSsoProviderAutoLinkingMutation(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerProvider = useMutation({
|
||||||
|
mutationFn: async (formData: ProviderForm) => {
|
||||||
|
const { error } = await authClient.sso.register({
|
||||||
|
providerId: formData.providerId,
|
||||||
|
issuer: formData.issuer,
|
||||||
|
domain: formData.domain,
|
||||||
|
organizationId: activeOrganization.id,
|
||||||
|
oidcConfig: {
|
||||||
|
clientId: formData.clientId,
|
||||||
|
clientSecret: formData.clientSecret,
|
||||||
|
discoveryEndpoint: formData.discoveryEndpoint,
|
||||||
|
scopes: ["openid", "email", "profile"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
await updateProviderAutoLinking
|
||||||
|
.mutateAsync({
|
||||||
|
path: { providerId: formData.providerId },
|
||||||
|
body: { enabled: formData.linkMatchingEmails },
|
||||||
|
})
|
||||||
|
.catch((updateError) => {
|
||||||
|
toast.warning("Auto-link setting could not be saved", {
|
||||||
|
description: parseError(updateError)?.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("SSO provider registered successfully");
|
||||||
|
void navigate({ to: "/settings", search: { tab: "users" } });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to register provider", { description: error.message });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>Register SSO Provider</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id={formId}
|
||||||
|
onSubmit={form.handleSubmit((values) => registerProvider.mutate(values))}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 @xl:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="providerId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Provider ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="acme-oidc" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Unique identifier used in callback URLs.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="domain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Organization Domain</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="example.com" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Used to discover providers during login.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="issuer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Issuer URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="https://idp.example.com" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="discoveryEndpoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Discovery Endpoint</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="https://idp.example.com/.well-known/openid-configuration"
|
||||||
|
disabled={registerProvider.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="oidc-client-id" disabled={registerProvider.isPending} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Secret</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
placeholder="oidc-client-secret"
|
||||||
|
disabled={registerProvider.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="linkMatchingEmails"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="rounded-md border p-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FormLabel>Link matching emails to existing accounts</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If enabled, users who sign in with this provider will automatically access their existing
|
||||||
|
account when the email address matches.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
disabled={registerProvider.isPending}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => void navigate({ to: "/settings", search: { tab: "users" } })}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" form={formId} loading={registerProvider.isPending}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Register Provider
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import { type AppContext } from "~/context";
|
||||||
import { TwoFactorSection } from "../components/two-factor-section";
|
import { TwoFactorSection } from "../components/two-factor-section";
|
||||||
import { UserManagement } from "../components/user-management";
|
import { UserManagement } from "../components/user-management";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { SsoSettingsSection } from "../components/sso/sso-settings-section";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appContext: AppContext;
|
appContext: AppContext;
|
||||||
|
|
@ -165,7 +166,7 @@ export function SettingsPage({ appContext }: Props) {
|
||||||
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
|
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="account">Account</TabsTrigger>
|
<TabsTrigger value="account">Account</TabsTrigger>
|
||||||
{isAdmin && <TabsTrigger value="users">Users</TabsTrigger>}
|
{isAdmin && <TabsTrigger value="users">Users & Authentication</TabsTrigger>}
|
||||||
{isAdmin && <TabsTrigger value="system">System</TabsTrigger>}
|
{isAdmin && <TabsTrigger value="system">System</TabsTrigger>}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|
@ -307,7 +308,7 @@ export function SettingsPage({ appContext }: Props) {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<TabsContent value="users" className="mt-0">
|
<TabsContent value="users" className="mt-0 space-y-4">
|
||||||
<Card className="p-0 gap-0">
|
<Card className="p-0 gap-0">
|
||||||
<div className="border-b border-border/50 bg-card-header p-6">
|
<div className="border-b border-border/50 bg-card-header p-6">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|
@ -316,7 +317,20 @@ export function SettingsPage({ appContext }: Props) {
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="mt-1.5">Manage users, roles and permissions</CardDescription>
|
<CardDescription className="mt-1.5">Manage users, roles and permissions</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<UserManagement />
|
<UserManagement currentUser={appContext.user} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-0 gap-0">
|
||||||
|
<div className="border-b border-border/50 bg-card-header p-6">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<SettingsIcon className="size-5" />
|
||||||
|
Single Sign-On
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-1.5">Configure OIDC provider settings</CardDescription>
|
||||||
|
</div>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<SsoSettingsSection />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ type User = {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
name: string;
|
||||||
hasDownloadedResticPassword: boolean;
|
hasDownloadedResticPassword: boolean;
|
||||||
twoFactorEnabled?: boolean | null;
|
twoFactorEnabled?: boolean | null;
|
||||||
role?: string | null | undefined;
|
role?: string | null | undefined;
|
||||||
|
|
|
||||||
15
app/drizzle/20260227200731_sad_luke_cage/migration.sql
Normal file
15
app/drizzle/20260227200731_sad_luke_cage/migration.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
CREATE TABLE `sso_provider` (
|
||||||
|
`id` text PRIMARY KEY,
|
||||||
|
`provider_id` text NOT NULL UNIQUE,
|
||||||
|
`organization_id` text NOT NULL,
|
||||||
|
`user_id` text,
|
||||||
|
`issuer` text NOT NULL,
|
||||||
|
`domain` text NOT NULL,
|
||||||
|
`auto_link_matching_emails` integer DEFAULT false NOT NULL,
|
||||||
|
`oidc_config` text,
|
||||||
|
`saml_config` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
CONSTRAINT `fk_sso_provider_organization_id_organization_id_fk` FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_sso_provider_user_id_users_table_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON DELETE SET NULL
|
||||||
|
);
|
||||||
2185
app/drizzle/20260227200731_sad_luke_cage/snapshot.json
Normal file
2185
app/drizzle/20260227200731_sad_luke_cage/snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,13 +4,20 @@ import { auth } from "~/server/lib/auth";
|
||||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||||
import { authService } from "~/server/modules/auth/auth.service";
|
import { authService } from "~/server/modules/auth/auth.service";
|
||||||
|
|
||||||
|
function isAuthRoute(pathname: string): boolean {
|
||||||
|
if (pathname === "/onboarding") return true;
|
||||||
|
if (pathname === "/login") return true;
|
||||||
|
if (pathname.match(/^\/login\/[^/]+$/)) return true;
|
||||||
|
if (pathname.match(/^\/login\/[^/]+\/error$/)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
|
export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
|
||||||
const headers = getRequestHeaders();
|
const headers = getRequestHeaders();
|
||||||
const session = await auth.api.getSession({ headers });
|
const session = await auth.api.getSession({ headers });
|
||||||
|
const pathname = new URL(request.url).pathname;
|
||||||
|
|
||||||
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
|
if (!session?.user?.id && !isAuthRoute(pathname)) {
|
||||||
|
|
||||||
if (!session?.user?.id && !isAuthRoute) {
|
|
||||||
const hasUsers = await authService.hasUsers();
|
const hasUsers = await authService.hasUsers();
|
||||||
if (!hasUsers) {
|
if (!hasUsers) {
|
||||||
throw redirect({ to: "/onboarding" });
|
throw redirect({ to: "/onboarding" });
|
||||||
|
|
@ -20,8 +27,12 @@ export const authMiddleware = createMiddleware().server(async ({ next, request }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session?.user?.id) {
|
if (session?.user?.id) {
|
||||||
if (isAuthRoute) {
|
if (!session.user.hasDownloadedResticPassword && pathname !== "/download-recovery-key") {
|
||||||
throw redirect({ to: "/" });
|
throw redirect({ to: "/download-recovery-key" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthRoute(pathname)) {
|
||||||
|
throw redirect({ to: "/volumes" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as dashboardRouteRouteImport } from './routes/(dashboard)/route'
|
import { Route as dashboardRouteRouteImport } from './routes/(dashboard)/route'
|
||||||
|
import { Route as authRouteRouteImport } from './routes/(auth)/route'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as ApiSplatRouteImport } from './routes/api.$'
|
import { Route as ApiSplatRouteImport } from './routes/api.$'
|
||||||
import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding'
|
import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding'
|
||||||
|
|
@ -26,8 +27,10 @@ import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashb
|
||||||
import { Route as dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create'
|
import { Route as dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create'
|
||||||
import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId'
|
import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId'
|
||||||
import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create'
|
import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create'
|
||||||
|
import { Route as authLoginErrorRouteImport } from './routes/(auth)/login.error'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index'
|
import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index'
|
||||||
import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index'
|
import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index'
|
||||||
|
import { Route as dashboardSettingsSsoNewRouteImport } from './routes/(dashboard)/settings/sso/new'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit'
|
import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/index'
|
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/index'
|
||||||
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore'
|
import { Route as dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore'
|
||||||
|
|
@ -37,6 +40,10 @@ const dashboardRouteRoute = dashboardRouteRouteImport.update({
|
||||||
id: '/(dashboard)',
|
id: '/(dashboard)',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const authRouteRoute = authRouteRouteImport.update({
|
||||||
|
id: '/(auth)',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
@ -48,19 +55,19 @@ const ApiSplatRoute = ApiSplatRouteImport.update({
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const authOnboardingRoute = authOnboardingRouteImport.update({
|
const authOnboardingRoute = authOnboardingRouteImport.update({
|
||||||
id: '/(auth)/onboarding',
|
id: '/onboarding',
|
||||||
path: '/onboarding',
|
path: '/onboarding',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => authRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const authLoginRoute = authLoginRouteImport.update({
|
const authLoginRoute = authLoginRouteImport.update({
|
||||||
id: '/(auth)/login',
|
id: '/login',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => authRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const authDownloadRecoveryKeyRoute = authDownloadRecoveryKeyRouteImport.update({
|
const authDownloadRecoveryKeyRoute = authDownloadRecoveryKeyRouteImport.update({
|
||||||
id: '/(auth)/download-recovery-key',
|
id: '/download-recovery-key',
|
||||||
path: '/download-recovery-key',
|
path: '/download-recovery-key',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => authRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({
|
const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({
|
||||||
id: '/volumes/',
|
id: '/volumes/',
|
||||||
|
|
@ -123,6 +130,11 @@ const dashboardBackupsCreateRoute = dashboardBackupsCreateRouteImport.update({
|
||||||
path: '/backups/create',
|
path: '/backups/create',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const authLoginErrorRoute = authLoginErrorRouteImport.update({
|
||||||
|
id: '/error',
|
||||||
|
path: '/error',
|
||||||
|
getParentRoute: () => authLoginRoute,
|
||||||
|
} as any)
|
||||||
const dashboardRepositoriesRepositoryIdIndexRoute =
|
const dashboardRepositoriesRepositoryIdIndexRoute =
|
||||||
dashboardRepositoriesRepositoryIdIndexRouteImport.update({
|
dashboardRepositoriesRepositoryIdIndexRouteImport.update({
|
||||||
id: '/repositories/$repositoryId/',
|
id: '/repositories/$repositoryId/',
|
||||||
|
|
@ -135,6 +147,11 @@ const dashboardBackupsBackupIdIndexRoute =
|
||||||
path: '/backups/$backupId/',
|
path: '/backups/$backupId/',
|
||||||
getParentRoute: () => dashboardRouteRoute,
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const dashboardSettingsSsoNewRoute = dashboardSettingsSsoNewRouteImport.update({
|
||||||
|
id: '/settings/sso/new',
|
||||||
|
path: '/settings/sso/new',
|
||||||
|
getParentRoute: () => dashboardRouteRoute,
|
||||||
|
} as any)
|
||||||
const dashboardRepositoriesRepositoryIdEditRoute =
|
const dashboardRepositoriesRepositoryIdEditRoute =
|
||||||
dashboardRepositoriesRepositoryIdEditRouteImport.update({
|
dashboardRepositoriesRepositoryIdEditRouteImport.update({
|
||||||
id: '/repositories/$repositoryId/edit',
|
id: '/repositories/$repositoryId/edit',
|
||||||
|
|
@ -163,9 +180,10 @@ const dashboardBackupsBackupIdSnapshotIdRestoreRoute =
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRouteWithChildren
|
||||||
'/onboarding': typeof authOnboardingRoute
|
'/onboarding': typeof authOnboardingRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
|
'/login/error': typeof authLoginErrorRoute
|
||||||
'/backups/create': typeof dashboardBackupsCreateRoute
|
'/backups/create': typeof dashboardBackupsCreateRoute
|
||||||
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
|
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
|
||||||
'/notifications/create': typeof dashboardNotificationsCreateRoute
|
'/notifications/create': typeof dashboardNotificationsCreateRoute
|
||||||
|
|
@ -178,6 +196,7 @@ export interface FileRoutesByFullPath {
|
||||||
'/settings/': typeof dashboardSettingsIndexRoute
|
'/settings/': typeof dashboardSettingsIndexRoute
|
||||||
'/volumes/': typeof dashboardVolumesIndexRoute
|
'/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute
|
||||||
'/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
'/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
||||||
'/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
'/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -187,9 +206,10 @@ export interface FileRoutesByFullPath {
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/login': typeof authLoginRoute
|
'/login': typeof authLoginRouteWithChildren
|
||||||
'/onboarding': typeof authOnboardingRoute
|
'/onboarding': typeof authOnboardingRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
|
'/login/error': typeof authLoginErrorRoute
|
||||||
'/backups/create': typeof dashboardBackupsCreateRoute
|
'/backups/create': typeof dashboardBackupsCreateRoute
|
||||||
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
|
'/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
|
||||||
'/notifications/create': typeof dashboardNotificationsCreateRoute
|
'/notifications/create': typeof dashboardNotificationsCreateRoute
|
||||||
|
|
@ -202,6 +222,7 @@ export interface FileRoutesByTo {
|
||||||
'/settings': typeof dashboardSettingsIndexRoute
|
'/settings': typeof dashboardSettingsIndexRoute
|
||||||
'/volumes': typeof dashboardVolumesIndexRoute
|
'/volumes': typeof dashboardVolumesIndexRoute
|
||||||
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
'/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
'/settings/sso/new': typeof dashboardSettingsSsoNewRoute
|
||||||
'/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute
|
'/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute
|
||||||
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
'/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -211,11 +232,13 @@ export interface FileRoutesByTo {
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/(auth)': typeof authRouteRouteWithChildren
|
||||||
'/(dashboard)': typeof dashboardRouteRouteWithChildren
|
'/(dashboard)': typeof dashboardRouteRouteWithChildren
|
||||||
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
'/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute
|
||||||
'/(auth)/login': typeof authLoginRoute
|
'/(auth)/login': typeof authLoginRouteWithChildren
|
||||||
'/(auth)/onboarding': typeof authOnboardingRoute
|
'/(auth)/onboarding': typeof authOnboardingRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
'/api/$': typeof ApiSplatRoute
|
||||||
|
'/(auth)/login/error': typeof authLoginErrorRoute
|
||||||
'/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute
|
'/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute
|
||||||
'/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
|
'/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute
|
||||||
'/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute
|
'/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute
|
||||||
|
|
@ -228,6 +251,7 @@ export interface FileRoutesById {
|
||||||
'/(dashboard)/settings/': typeof dashboardSettingsIndexRoute
|
'/(dashboard)/settings/': typeof dashboardSettingsIndexRoute
|
||||||
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
|
||||||
'/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
'/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
'/(dashboard)/settings/sso/new': typeof dashboardSettingsSsoNewRoute
|
||||||
'/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
'/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute
|
||||||
'/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
'/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -242,6 +266,7 @@ export interface FileRouteTypes {
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
| '/api/$'
|
| '/api/$'
|
||||||
|
| '/login/error'
|
||||||
| '/backups/create'
|
| '/backups/create'
|
||||||
| '/notifications/$notificationId'
|
| '/notifications/$notificationId'
|
||||||
| '/notifications/create'
|
| '/notifications/create'
|
||||||
|
|
@ -254,6 +279,7 @@ export interface FileRouteTypes {
|
||||||
| '/settings/'
|
| '/settings/'
|
||||||
| '/volumes/'
|
| '/volumes/'
|
||||||
| '/repositories/$repositoryId/edit'
|
| '/repositories/$repositoryId/edit'
|
||||||
|
| '/settings/sso/new'
|
||||||
| '/backups/$backupId/'
|
| '/backups/$backupId/'
|
||||||
| '/repositories/$repositoryId/'
|
| '/repositories/$repositoryId/'
|
||||||
| '/backups/$backupId/$snapshotId/restore'
|
| '/backups/$backupId/$snapshotId/restore'
|
||||||
|
|
@ -266,6 +292,7 @@ export interface FileRouteTypes {
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboarding'
|
| '/onboarding'
|
||||||
| '/api/$'
|
| '/api/$'
|
||||||
|
| '/login/error'
|
||||||
| '/backups/create'
|
| '/backups/create'
|
||||||
| '/notifications/$notificationId'
|
| '/notifications/$notificationId'
|
||||||
| '/notifications/create'
|
| '/notifications/create'
|
||||||
|
|
@ -278,6 +305,7 @@ export interface FileRouteTypes {
|
||||||
| '/settings'
|
| '/settings'
|
||||||
| '/volumes'
|
| '/volumes'
|
||||||
| '/repositories/$repositoryId/edit'
|
| '/repositories/$repositoryId/edit'
|
||||||
|
| '/settings/sso/new'
|
||||||
| '/backups/$backupId'
|
| '/backups/$backupId'
|
||||||
| '/repositories/$repositoryId'
|
| '/repositories/$repositoryId'
|
||||||
| '/backups/$backupId/$snapshotId/restore'
|
| '/backups/$backupId/$snapshotId/restore'
|
||||||
|
|
@ -286,11 +314,13 @@ export interface FileRouteTypes {
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/(auth)'
|
||||||
| '/(dashboard)'
|
| '/(dashboard)'
|
||||||
| '/(auth)/download-recovery-key'
|
| '/(auth)/download-recovery-key'
|
||||||
| '/(auth)/login'
|
| '/(auth)/login'
|
||||||
| '/(auth)/onboarding'
|
| '/(auth)/onboarding'
|
||||||
| '/api/$'
|
| '/api/$'
|
||||||
|
| '/(auth)/login/error'
|
||||||
| '/(dashboard)/backups/create'
|
| '/(dashboard)/backups/create'
|
||||||
| '/(dashboard)/notifications/$notificationId'
|
| '/(dashboard)/notifications/$notificationId'
|
||||||
| '/(dashboard)/notifications/create'
|
| '/(dashboard)/notifications/create'
|
||||||
|
|
@ -303,6 +333,7 @@ export interface FileRouteTypes {
|
||||||
| '/(dashboard)/settings/'
|
| '/(dashboard)/settings/'
|
||||||
| '/(dashboard)/volumes/'
|
| '/(dashboard)/volumes/'
|
||||||
| '/(dashboard)/repositories/$repositoryId/edit'
|
| '/(dashboard)/repositories/$repositoryId/edit'
|
||||||
|
| '/(dashboard)/settings/sso/new'
|
||||||
| '/(dashboard)/backups/$backupId/'
|
| '/(dashboard)/backups/$backupId/'
|
||||||
| '/(dashboard)/repositories/$repositoryId/'
|
| '/(dashboard)/repositories/$repositoryId/'
|
||||||
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
|
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
|
||||||
|
|
@ -312,10 +343,8 @@ export interface FileRouteTypes {
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
authRouteRoute: typeof authRouteRouteWithChildren
|
||||||
dashboardRouteRoute: typeof dashboardRouteRouteWithChildren
|
dashboardRouteRoute: typeof dashboardRouteRouteWithChildren
|
||||||
authDownloadRecoveryKeyRoute: typeof authDownloadRecoveryKeyRoute
|
|
||||||
authLoginRoute: typeof authLoginRoute
|
|
||||||
authOnboardingRoute: typeof authOnboardingRoute
|
|
||||||
ApiSplatRoute: typeof ApiSplatRoute
|
ApiSplatRoute: typeof ApiSplatRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,6 +357,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardRouteRouteImport
|
preLoaderRoute: typeof dashboardRouteRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/(auth)': {
|
||||||
|
id: '/(auth)'
|
||||||
|
path: ''
|
||||||
|
fullPath: ''
|
||||||
|
preLoaderRoute: typeof authRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
|
|
@ -347,21 +383,21 @@ declare module '@tanstack/react-router' {
|
||||||
path: '/onboarding'
|
path: '/onboarding'
|
||||||
fullPath: '/onboarding'
|
fullPath: '/onboarding'
|
||||||
preLoaderRoute: typeof authOnboardingRouteImport
|
preLoaderRoute: typeof authOnboardingRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof authRouteRoute
|
||||||
}
|
}
|
||||||
'/(auth)/login': {
|
'/(auth)/login': {
|
||||||
id: '/(auth)/login'
|
id: '/(auth)/login'
|
||||||
path: '/login'
|
path: '/login'
|
||||||
fullPath: '/login'
|
fullPath: '/login'
|
||||||
preLoaderRoute: typeof authLoginRouteImport
|
preLoaderRoute: typeof authLoginRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof authRouteRoute
|
||||||
}
|
}
|
||||||
'/(auth)/download-recovery-key': {
|
'/(auth)/download-recovery-key': {
|
||||||
id: '/(auth)/download-recovery-key'
|
id: '/(auth)/download-recovery-key'
|
||||||
path: '/download-recovery-key'
|
path: '/download-recovery-key'
|
||||||
fullPath: '/download-recovery-key'
|
fullPath: '/download-recovery-key'
|
||||||
preLoaderRoute: typeof authDownloadRecoveryKeyRouteImport
|
preLoaderRoute: typeof authDownloadRecoveryKeyRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof authRouteRoute
|
||||||
}
|
}
|
||||||
'/(dashboard)/volumes/': {
|
'/(dashboard)/volumes/': {
|
||||||
id: '/(dashboard)/volumes/'
|
id: '/(dashboard)/volumes/'
|
||||||
|
|
@ -440,6 +476,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardBackupsCreateRouteImport
|
preLoaderRoute: typeof dashboardBackupsCreateRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
parentRoute: typeof dashboardRouteRoute
|
||||||
}
|
}
|
||||||
|
'/(auth)/login/error': {
|
||||||
|
id: '/(auth)/login/error'
|
||||||
|
path: '/error'
|
||||||
|
fullPath: '/login/error'
|
||||||
|
preLoaderRoute: typeof authLoginErrorRouteImport
|
||||||
|
parentRoute: typeof authLoginRoute
|
||||||
|
}
|
||||||
'/(dashboard)/repositories/$repositoryId/': {
|
'/(dashboard)/repositories/$repositoryId/': {
|
||||||
id: '/(dashboard)/repositories/$repositoryId/'
|
id: '/(dashboard)/repositories/$repositoryId/'
|
||||||
path: '/repositories/$repositoryId'
|
path: '/repositories/$repositoryId'
|
||||||
|
|
@ -454,6 +497,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport
|
preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport
|
||||||
parentRoute: typeof dashboardRouteRoute
|
parentRoute: typeof dashboardRouteRoute
|
||||||
}
|
}
|
||||||
|
'/(dashboard)/settings/sso/new': {
|
||||||
|
id: '/(dashboard)/settings/sso/new'
|
||||||
|
path: '/settings/sso/new'
|
||||||
|
fullPath: '/settings/sso/new'
|
||||||
|
preLoaderRoute: typeof dashboardSettingsSsoNewRouteImport
|
||||||
|
parentRoute: typeof dashboardRouteRoute
|
||||||
|
}
|
||||||
'/(dashboard)/repositories/$repositoryId/edit': {
|
'/(dashboard)/repositories/$repositoryId/edit': {
|
||||||
id: '/(dashboard)/repositories/$repositoryId/edit'
|
id: '/(dashboard)/repositories/$repositoryId/edit'
|
||||||
path: '/repositories/$repositoryId/edit'
|
path: '/repositories/$repositoryId/edit'
|
||||||
|
|
@ -485,6 +535,34 @@ declare module '@tanstack/react-router' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface authLoginRouteChildren {
|
||||||
|
authLoginErrorRoute: typeof authLoginErrorRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const authLoginRouteChildren: authLoginRouteChildren = {
|
||||||
|
authLoginErrorRoute: authLoginErrorRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const authLoginRouteWithChildren = authLoginRoute._addFileChildren(
|
||||||
|
authLoginRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface authRouteRouteChildren {
|
||||||
|
authDownloadRecoveryKeyRoute: typeof authDownloadRecoveryKeyRoute
|
||||||
|
authLoginRoute: typeof authLoginRouteWithChildren
|
||||||
|
authOnboardingRoute: typeof authOnboardingRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const authRouteRouteChildren: authRouteRouteChildren = {
|
||||||
|
authDownloadRecoveryKeyRoute: authDownloadRecoveryKeyRoute,
|
||||||
|
authLoginRoute: authLoginRouteWithChildren,
|
||||||
|
authOnboardingRoute: authOnboardingRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const authRouteRouteWithChildren = authRouteRoute._addFileChildren(
|
||||||
|
authRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
interface dashboardRouteRouteChildren {
|
interface dashboardRouteRouteChildren {
|
||||||
dashboardBackupsCreateRoute: typeof dashboardBackupsCreateRoute
|
dashboardBackupsCreateRoute: typeof dashboardBackupsCreateRoute
|
||||||
dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute
|
dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute
|
||||||
|
|
@ -498,6 +576,7 @@ interface dashboardRouteRouteChildren {
|
||||||
dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute
|
dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute
|
||||||
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
|
||||||
dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute
|
dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute
|
||||||
|
dashboardSettingsSsoNewRoute: typeof dashboardSettingsSsoNewRoute
|
||||||
dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute
|
dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute
|
||||||
dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute
|
dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute
|
||||||
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
|
||||||
|
|
@ -520,6 +599,7 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
||||||
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
|
||||||
dashboardRepositoriesRepositoryIdEditRoute:
|
dashboardRepositoriesRepositoryIdEditRoute:
|
||||||
dashboardRepositoriesRepositoryIdEditRoute,
|
dashboardRepositoriesRepositoryIdEditRoute,
|
||||||
|
dashboardSettingsSsoNewRoute: dashboardSettingsSsoNewRoute,
|
||||||
dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute,
|
dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute,
|
||||||
dashboardRepositoriesRepositoryIdIndexRoute:
|
dashboardRepositoriesRepositoryIdIndexRoute:
|
||||||
dashboardRepositoriesRepositoryIdIndexRoute,
|
dashboardRepositoriesRepositoryIdIndexRoute,
|
||||||
|
|
@ -537,10 +617,8 @@ const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren(
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
authRouteRoute: authRouteRouteWithChildren,
|
||||||
dashboardRouteRoute: dashboardRouteRouteWithChildren,
|
dashboardRouteRoute: dashboardRouteRouteWithChildren,
|
||||||
authDownloadRecoveryKeyRoute: authDownloadRecoveryKeyRoute,
|
|
||||||
authLoginRoute: authLoginRoute,
|
|
||||||
authOnboardingRoute: authOnboardingRoute,
|
|
||||||
ApiSplatRoute: ApiSplatRoute,
|
ApiSplatRoute: ApiSplatRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
|
|
|
||||||
23
app/routes/(auth)/login.error.tsx
Normal file
23
app/routes/(auth)/login.error.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { type } from "arktype";
|
||||||
|
import { LoginPage } from "~/client/modules/auth/routes/login";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(auth)/login/error")({
|
||||||
|
component: RouteComponent,
|
||||||
|
validateSearch: type({ error: "string?" }),
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ title: "Zerobyte - Login Error" },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Resolve SSO sign-in errors.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { error } = Route.useSearch();
|
||||||
|
|
||||||
|
return <LoginPage error={error} />;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute, useRouterState } from "@tanstack/react-router";
|
||||||
|
import { type } from "arktype";
|
||||||
import { LoginPage } from "~/client/modules/auth/routes/login";
|
import { LoginPage } from "~/client/modules/auth/routes/login";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(auth)/login")({
|
export const Route = createFileRoute("/(auth)/login")({
|
||||||
component: LoginPage,
|
component: RouteComponent,
|
||||||
|
validateSearch: type({ error: "string?" }),
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [
|
meta: [
|
||||||
{ title: "Zerobyte - Login" },
|
{ title: "Zerobyte - Login" },
|
||||||
|
|
@ -13,3 +15,14 @@ export const Route = createFileRoute("/(auth)/login")({
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { error } = Route.useSearch();
|
||||||
|
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
||||||
|
|
||||||
|
if (pathname !== "/login") {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LoginPage error={error} />;
|
||||||
|
}
|
||||||
|
|
|
||||||
9
app/routes/(auth)/route.tsx
Normal file
9
app/routes/(auth)/route.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { authMiddleware } from "~/middleware/auth";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(auth)")({
|
||||||
|
component: () => <Outlet />,
|
||||||
|
server: {
|
||||||
|
middleware: [authMiddleware],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -3,12 +3,21 @@ import { fetchUser } from "../route";
|
||||||
import type { AppContext } from "~/context";
|
import type { AppContext } from "~/context";
|
||||||
import { SettingsPage } from "~/client/modules/settings/routes/settings";
|
import { SettingsPage } from "~/client/modules/settings/routes/settings";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import { getAdminUsersOptions, getSsoSettingsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
export const Route = createFileRoute("/(dashboard)/settings/")({
|
export const Route = createFileRoute("/(dashboard)/settings/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
validateSearch: type({ tab: "string?" }),
|
validateSearch: type({ tab: "string?" }),
|
||||||
loader: async () => {
|
loader: async ({ context }) => {
|
||||||
const authContext = await fetchUser();
|
const authContext = await fetchUser();
|
||||||
|
|
||||||
|
if (authContext.user?.role === "admin") {
|
||||||
|
await Promise.all([
|
||||||
|
context.queryClient.ensureQueryData(getSsoSettingsOptions()),
|
||||||
|
context.queryClient.ensureQueryData(getAdminUsersOptions()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return authContext as AppContext;
|
return authContext as AppContext;
|
||||||
},
|
},
|
||||||
staticData: {
|
staticData: {
|
||||||
|
|
|
||||||
32
app/routes/(dashboard)/settings/sso/new.tsx
Normal file
32
app/routes/(dashboard)/settings/sso/new.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
import { CreateSsoProviderPage } from "~/client/modules/settings/routes/create-sso-provider";
|
||||||
|
import { fetchUser } from "../../route";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(dashboard)/settings/sso/new")({
|
||||||
|
component: RouteComponent,
|
||||||
|
loader: async () => {
|
||||||
|
const authContext = await fetchUser();
|
||||||
|
|
||||||
|
if (authContext.user?.role !== "admin") {
|
||||||
|
throw redirect({ to: "/settings" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return authContext;
|
||||||
|
},
|
||||||
|
staticData: {
|
||||||
|
breadcrumb: () => [{ label: "Settings", href: "/settings" }, { label: "Register SSO Provider" }],
|
||||||
|
},
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{ title: "Zerobyte - Register SSO Provider" },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Register a new OIDC identity provider for organization sign-in.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <CreateSsoProviderPage />;
|
||||||
|
}
|
||||||
|
|
@ -8,11 +8,7 @@ const handle = ({ request }: { request: Request }) => app.fetch(request.clone())
|
||||||
export const Route = createFileRoute("/api/$")({
|
export const Route = createFileRoute("/api/$")({
|
||||||
server: {
|
server: {
|
||||||
handlers: {
|
handlers: {
|
||||||
GET: handle,
|
ANY: handle,
|
||||||
POST: handle,
|
|
||||||
DELETE: handle,
|
|
||||||
PUT: handle,
|
|
||||||
PATCH: handle,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export const relations = defineRelations(schema, (r) => ({
|
||||||
sessions: r.many.sessionsTable(),
|
sessions: r.many.sessionsTable(),
|
||||||
members: r.many.member(),
|
members: r.many.member(),
|
||||||
twoFactors: r.many.twoFactor(),
|
twoFactors: r.many.twoFactor(),
|
||||||
|
ssoProviders: r.many.ssoProvider(),
|
||||||
organizations: r.many.organization({
|
organizations: r.many.organization({
|
||||||
from: r.usersTable.id.through(r.member.userId),
|
from: r.usersTable.id.through(r.member.userId),
|
||||||
to: r.organization.id.through(r.member.organizationId),
|
to: r.organization.id.through(r.member.organizationId),
|
||||||
|
|
@ -104,6 +105,19 @@ export const relations = defineRelations(schema, (r) => ({
|
||||||
volumes: r.many.volumesTable(),
|
volumes: r.many.volumesTable(),
|
||||||
members: r.many.member(),
|
members: r.many.member(),
|
||||||
invitations: r.many.invitation(),
|
invitations: r.many.invitation(),
|
||||||
|
ssoProviders: r.many.ssoProvider(),
|
||||||
|
},
|
||||||
|
ssoProvider: {
|
||||||
|
user: r.one.usersTable({
|
||||||
|
from: r.ssoProvider.userId,
|
||||||
|
to: r.usersTable.id,
|
||||||
|
optional: true,
|
||||||
|
}),
|
||||||
|
organization: r.one.organization({
|
||||||
|
from: r.ssoProvider.organizationId,
|
||||||
|
to: r.organization.id,
|
||||||
|
optional: false,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
volumesTable: {
|
volumesTable: {
|
||||||
backupSchedules: r.many.backupSchedulesTable(),
|
backupSchedules: r.many.backupSchedulesTable(),
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,27 @@ export const invitation = sqliteTable(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ssoProvider = sqliteTable("sso_provider", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
providerId: text("provider_id").notNull().unique(),
|
||||||
|
organizationId: text("organization_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
|
userId: text("user_id").references(() => usersTable.id, { onDelete: "set null" }),
|
||||||
|
issuer: text("issuer").notNull(),
|
||||||
|
domain: text("domain").notNull(),
|
||||||
|
autoLinkMatchingEmails: int("auto_link_matching_emails", { mode: "boolean" }).notNull().default(false),
|
||||||
|
oidcConfig: text("oidc_config", { mode: "json" }).$type<Record<string, unknown> | null>(),
|
||||||
|
samlConfig: text("saml_config", { mode: "json" }).$type<Record<string, unknown> | null>(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.notNull()
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Volumes Table
|
* Volumes Table
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,24 @@ import {
|
||||||
type BetterAuthOptions,
|
type BetterAuthOptions,
|
||||||
type MiddlewareContext,
|
type MiddlewareContext,
|
||||||
type MiddlewareOptions,
|
type MiddlewareOptions,
|
||||||
|
type User,
|
||||||
} from "better-auth";
|
} from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { admin, createAuthMiddleware, twoFactor, username, organization } from "better-auth/plugins";
|
import { admin, createAuthMiddleware, twoFactor, username, organization } from "better-auth/plugins";
|
||||||
import { UnauthorizedError } from "http-errors-enhanced";
|
import { sso } from "@better-auth/sso";
|
||||||
import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { config } from "../core/config";
|
import { config } from "../core/config";
|
||||||
import { db } from "../db/db";
|
import { db } from "../db/db";
|
||||||
import { cryptoUtils } from "../utils/crypto";
|
import { cryptoUtils } from "../utils/crypto";
|
||||||
import { organization as organizationTable, member, usersTable } from "../db/schema";
|
|
||||||
import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user";
|
|
||||||
import { authService } from "../modules/auth/auth.service";
|
import { authService } from "../modules/auth/auth.service";
|
||||||
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
||||||
import { isValidUsername, normalizeUsername } from "~/lib/username";
|
import { isValidUsername, normalizeUsername } from "~/lib/username";
|
||||||
|
import { ensureOnlyOneUser } from "./auth/middlewares/only-one-user";
|
||||||
|
import { convertLegacyUserOnFirstLogin } from "./auth/middlewares/convert-legacy-user";
|
||||||
|
import { validateSsoCallbackUrls } from "./auth/middlewares/validate-sso-callback-urls";
|
||||||
|
import { validateSsoProviderId } from "./auth/middlewares/validate-sso-provider-id";
|
||||||
|
import { createUserDefaultOrg } from "./auth/helpers/create-default-org";
|
||||||
|
import { isSsoCallbackRequest, requireSsoInvitation } from "./auth/middlewares/require-sso-invitation";
|
||||||
|
import { ssoTrustedProviderLinkingPlugin } from "./auth/plugins/sso-trusted-provider-linking";
|
||||||
|
|
||||||
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
||||||
|
|
||||||
|
|
@ -34,6 +38,8 @@ export const auth = betterAuth({
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
before: createAuthMiddleware(async (ctx) => {
|
before: createAuthMiddleware(async (ctx) => {
|
||||||
|
await validateSsoProviderId(ctx);
|
||||||
|
await validateSsoCallbackUrls(ctx);
|
||||||
await ensureOnlyOneUser(ctx);
|
await ensureOnlyOneUser(ctx);
|
||||||
await convertLegacyUserOnFirstLogin(ctx);
|
await convertLegacyUserOnFirstLogin(ctx);
|
||||||
}),
|
}),
|
||||||
|
|
@ -49,7 +55,12 @@ export const auth = betterAuth({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
before: async (user) => {
|
before: async (user, ctx) => {
|
||||||
|
if (isSsoCallbackRequest(ctx)) {
|
||||||
|
await requireSsoInvitation(user.email, ctx);
|
||||||
|
user.hasDownloadedResticPassword = true;
|
||||||
|
}
|
||||||
|
|
||||||
const anyUser = await db.query.usersTable.findFirst();
|
const anyUser = await db.query.usersTable.findFirst();
|
||||||
const isFirstUser = !anyUser;
|
const isFirstUser = !anyUser;
|
||||||
|
|
||||||
|
|
@ -57,65 +68,19 @@ export const auth = betterAuth({
|
||||||
user.role = "admin";
|
user.role = "admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: user };
|
if (!user.username) {
|
||||||
},
|
user.username = Bun.randomUUIDv7();
|
||||||
after: async (user) => {
|
|
||||||
const slug = user.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4);
|
|
||||||
|
|
||||||
const resticPassword = cryptoUtils.generateResticPassword();
|
|
||||||
const metadata = {
|
|
||||||
resticPassword: await cryptoUtils.sealSecret(resticPassword),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
db.transaction((tx) => {
|
|
||||||
const orgId = Bun.randomUUIDv7();
|
|
||||||
|
|
||||||
tx.insert(organizationTable)
|
|
||||||
.values({
|
|
||||||
name: `${user.name}'s Workspace`,
|
|
||||||
slug: slug,
|
|
||||||
id: orgId,
|
|
||||||
createdAt: new Date(),
|
|
||||||
metadata,
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
tx.insert(member)
|
|
||||||
.values({
|
|
||||||
id: Bun.randomUUIDv7(),
|
|
||||||
userId: user.id,
|
|
||||||
role: "owner",
|
|
||||||
organizationId: orgId,
|
|
||||||
createdAt: new Date(),
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
await db.delete(usersTable).where(eq(usersTable.id, user.id));
|
|
||||||
|
|
||||||
throw new Error(`Failed to create organization for user ${user.id}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { data: user };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
create: {
|
create: {
|
||||||
before: async (session) => {
|
before: async (session, ctx) => {
|
||||||
const orgMembership = await db.query.member.findFirst({
|
const membership = await createUserDefaultOrg(session.userId, ctx);
|
||||||
where: { userId: session.userId },
|
return { data: { ...session, activeOrganizationId: membership.organizationId } };
|
||||||
});
|
|
||||||
|
|
||||||
if (!orgMembership) {
|
|
||||||
throw new UnauthorizedError("User does not belong to any organization");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
...session,
|
|
||||||
activeOrganizationId: orgMembership?.organizationId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -123,6 +88,11 @@ export const auth = betterAuth({
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
account: {
|
||||||
|
accountLinking: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
modelName: "usersTable",
|
modelName: "usersTable",
|
||||||
additionalFields: {
|
additionalFields: {
|
||||||
|
|
@ -151,6 +121,22 @@ export const auth = betterAuth({
|
||||||
organization({
|
organization({
|
||||||
allowUserToCreateOrganization: false,
|
allowUserToCreateOrganization: false,
|
||||||
}),
|
}),
|
||||||
|
sso({
|
||||||
|
trustEmailVerified: false,
|
||||||
|
providersLimit: async (user: User) => {
|
||||||
|
const existingUser = await db.query.usersTable.findFirst({
|
||||||
|
columns: { role: true },
|
||||||
|
where: { id: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return existingUser?.role === "admin" ? 10 : 0;
|
||||||
|
},
|
||||||
|
organizationProvisioning: {
|
||||||
|
disabled: false,
|
||||||
|
defaultRole: "member",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ssoTrustedProviderLinkingPlugin(),
|
||||||
twoFactor({
|
twoFactor({
|
||||||
backupCodeOptions: {
|
backupCodeOptions: {
|
||||||
storeBackupCodes: "encrypted",
|
storeBackupCodes: "encrypted",
|
||||||
|
|
|
||||||
161
app/server/lib/auth/helpers/__tests__/create-default-org.test.ts
Normal file
161
app/server/lib/auth/helpers/__tests__/create-default-org.test.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import { createUserDefaultOrg } from "../create-default-org";
|
||||||
|
|
||||||
|
function createMockContext(path: string, params: Record<string, string> = {}): GenericEndpointContext {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
body: {},
|
||||||
|
query: {},
|
||||||
|
headers: new Headers(),
|
||||||
|
request: new Request(`http://localhost:3000${path}`),
|
||||||
|
params,
|
||||||
|
method: "POST",
|
||||||
|
context: {} as GenericEndpointContext["context"],
|
||||||
|
} as GenericEndpointContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockSsoCallbackContext(providerId: string): GenericEndpointContext {
|
||||||
|
return createMockContext(`/sso/callback/${providerId}`, { providerId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
return Bun.randomUUIDv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomSlug(prefix: string) {
|
||||||
|
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser(email: string, username: string) {
|
||||||
|
const userId = randomId();
|
||||||
|
await db.insert(usersTable).values({
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
name: username,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("createUserDefaultOrg", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.delete(member);
|
||||||
|
await db.delete(account);
|
||||||
|
await db.delete(invitation);
|
||||||
|
await db.delete(ssoProvider);
|
||||||
|
await db.delete(organization);
|
||||||
|
await db.delete(usersTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates invited membership from SSO callback request context", async () => {
|
||||||
|
const invitedUserId = await createUser("invited@example.com", randomSlug("invited"));
|
||||||
|
const inviterId = await createUser("inviter@example.com", randomSlug("inviter"));
|
||||||
|
const organizationId = randomId();
|
||||||
|
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id: organizationId,
|
||||||
|
name: "Acme",
|
||||||
|
slug: randomSlug("acme"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId: "oidc-acme",
|
||||||
|
organizationId,
|
||||||
|
userId: inviterId,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(invitation).values({
|
||||||
|
id: randomId(),
|
||||||
|
organizationId,
|
||||||
|
email: "invited@example.com",
|
||||||
|
role: "member",
|
||||||
|
status: "pending",
|
||||||
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||||
|
createdAt: new Date(),
|
||||||
|
inviterId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = createMockSsoCallbackContext("oidc-acme");
|
||||||
|
const membership = await createUserDefaultOrg(invitedUserId, ctx);
|
||||||
|
|
||||||
|
expect(membership.organizationId).toBe(organizationId);
|
||||||
|
expect(membership.role).toBe("member");
|
||||||
|
|
||||||
|
const updatedInvitations = await db.select().from(invitation).where(eq(invitation.organizationId, organizationId));
|
||||||
|
const updatedInvitation = updatedInvitations.find((i) => i.email === "invited@example.com");
|
||||||
|
expect(updatedInvitation?.status).toBe("accepted");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks SSO callback users without pending invitations", async () => {
|
||||||
|
const userId = await createUser("new-user@example.com", randomSlug("new-user"));
|
||||||
|
const inviterId = await createUser("inviter@example.com", randomSlug("inviter"));
|
||||||
|
const organizationId = randomId();
|
||||||
|
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id: organizationId,
|
||||||
|
name: "Acme",
|
||||||
|
slug: randomSlug("acme"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId: "oidc-acme",
|
||||||
|
organizationId,
|
||||||
|
userId: inviterId,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = createMockSsoCallbackContext("oidc-acme");
|
||||||
|
expect(createUserDefaultOrg(userId, ctx)).rejects.toThrow("invite-only");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns existing membership without creating another workspace", async () => {
|
||||||
|
const userId = await createUser("existing-member@example.com", randomSlug("existing-member"));
|
||||||
|
const organizationId = randomId();
|
||||||
|
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id: organizationId,
|
||||||
|
name: "Existing Org",
|
||||||
|
slug: randomSlug("existing"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(member).values({
|
||||||
|
id: randomId(),
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
role: "owner",
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const membership = await createUserDefaultOrg(userId, null);
|
||||||
|
|
||||||
|
expect(membership.organizationId).toBe(organizationId);
|
||||||
|
expect(membership.role).toBe("owner");
|
||||||
|
|
||||||
|
const memberships = await db.select().from(member).where(eq(member.userId, userId));
|
||||||
|
expect(memberships.length).toBe(1);
|
||||||
|
|
||||||
|
const organizations = await db.select().from(organization);
|
||||||
|
expect(organizations.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates personal workspace for non-SSO flows", async () => {
|
||||||
|
const userId = await createUser("local-user@example.com", randomSlug("local-user"));
|
||||||
|
|
||||||
|
const membership = await createUserDefaultOrg(userId, null);
|
||||||
|
|
||||||
|
expect(membership.role).toBe("owner");
|
||||||
|
expect(membership.organization.name).toContain("Workspace");
|
||||||
|
});
|
||||||
|
});
|
||||||
172
app/server/lib/auth/helpers/create-default-org.ts
Normal file
172
app/server/lib/auth/helpers/create-default-org.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { and, eq, gt } from "drizzle-orm";
|
||||||
|
import { UnauthorizedError } from "http-errors-enhanced";
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { invitation, member, organization, ssoProvider, usersTable, type User } from "~/server/db/schema";
|
||||||
|
import { cryptoUtils } from "~/server/utils/crypto";
|
||||||
|
import { APIError } from "better-auth";
|
||||||
|
import { extractProviderIdFromContext, normalizeEmail } from "../utils/sso-context";
|
||||||
|
import { logger } from "~/server/utils/logger";
|
||||||
|
|
||||||
|
export async function findMembershipWithOrganization(userId: string, organizationId?: string) {
|
||||||
|
const memberships = await db
|
||||||
|
.select()
|
||||||
|
.from(member)
|
||||||
|
.where(
|
||||||
|
organizationId
|
||||||
|
? and(eq(member.userId, userId), eq(member.organizationId, organizationId))
|
||||||
|
: eq(member.userId, userId),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const membership = memberships[0];
|
||||||
|
|
||||||
|
if (!membership) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgs = await db.select().from(organization).where(eq(organization.id, membership.organizationId)).limit(1);
|
||||||
|
const org = orgs[0];
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...membership, organization: org };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOrgSlug(email: string) {
|
||||||
|
const [emailPrefix] = email.split("@");
|
||||||
|
const sanitized = emailPrefix
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
const safePrefix = sanitized || "org";
|
||||||
|
return `${safePrefix}-${Math.random().toString(36).slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryCreateInvitedMembership(userId: string, email: string, ctx: GenericEndpointContext | null) {
|
||||||
|
logger.debug("Checking for pending invitations for user", userId);
|
||||||
|
|
||||||
|
const providerId = extractProviderIdFromContext(ctx);
|
||||||
|
const ssoProviders = await db.select().from(ssoProvider).where(eq(ssoProvider.providerId, providerId)).limit(1);
|
||||||
|
const ssoProviderRecord = ssoProviders[0];
|
||||||
|
|
||||||
|
if (!ssoProviderRecord) {
|
||||||
|
logger.debug("No SSO provider found in context, skipping invitation check");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
logger.debug("SSO provider found in context, checking for linked accounts", ssoProviderRecord.providerId);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const pendingInvitations = await db
|
||||||
|
.select({
|
||||||
|
id: invitation.id,
|
||||||
|
email: invitation.email,
|
||||||
|
role: invitation.role,
|
||||||
|
organizationId: invitation.organizationId,
|
||||||
|
})
|
||||||
|
.from(invitation)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(invitation.status, "pending"),
|
||||||
|
eq(invitation.organizationId, ssoProviderRecord.organizationId),
|
||||||
|
gt(invitation.expiresAt, now),
|
||||||
|
eq(invitation.email, normalizeEmail(email)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
const pendingInvitation = pendingInvitations[0];
|
||||||
|
|
||||||
|
if (!pendingInvitation) {
|
||||||
|
logger.debug("No pending invitation found for user");
|
||||||
|
throw new APIError("FORBIDDEN", { message: "SSO sign-in is invite-only for this organization" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
tx.insert(member)
|
||||||
|
.values({
|
||||||
|
id: Bun.randomUUIDv7(),
|
||||||
|
userId,
|
||||||
|
role: pendingInvitation.role as "member",
|
||||||
|
organizationId: pendingInvitation.organizationId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
tx.update(invitation).set({ status: "accepted" }).where(eq(invitation.id, pendingInvitation.id)).run();
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitedMembership = await findMembershipWithOrganization(userId, pendingInvitation.organizationId);
|
||||||
|
logger.debug("Created organization membership from invitation", {
|
||||||
|
userId,
|
||||||
|
organizationId: pendingInvitation.organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!invitedMembership) {
|
||||||
|
throw new Error("Failed to create invited organization membership");
|
||||||
|
}
|
||||||
|
|
||||||
|
return invitedMembership;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDefaultOrganizationMembership(user: User) {
|
||||||
|
logger.debug("Creating default organization for user", { userId: user.id });
|
||||||
|
const resticPassword = cryptoUtils.generateResticPassword();
|
||||||
|
const metadata = { resticPassword: await cryptoUtils.sealSecret(resticPassword) };
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const orgId = Bun.randomUUIDv7();
|
||||||
|
const slug = buildOrgSlug(user.email);
|
||||||
|
|
||||||
|
tx.insert(organization)
|
||||||
|
.values({
|
||||||
|
name: `${user.name}'s Workspace`,
|
||||||
|
slug,
|
||||||
|
id: orgId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
tx.insert(member)
|
||||||
|
.values({
|
||||||
|
id: Bun.randomUUIDv7(),
|
||||||
|
userId: user.id,
|
||||||
|
role: "owner",
|
||||||
|
organizationId: orgId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserDefaultOrg(userId: string, ctx: GenericEndpointContext | null) {
|
||||||
|
const users = await db.select().from(usersTable).where(eq(usersTable.id, userId)).limit(1);
|
||||||
|
const user = users[0];
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedError("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMembership = await findMembershipWithOrganization(user.id);
|
||||||
|
if (existingMembership) {
|
||||||
|
logger.debug("User already has an organization membership, skipping default org creation", { userId });
|
||||||
|
return existingMembership;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invitedMembership = await tryCreateInvitedMembership(userId, normalizeEmail(user.email), ctx);
|
||||||
|
if (invitedMembership) {
|
||||||
|
return invitedMembership;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createDefaultOrganizationMembership(user);
|
||||||
|
|
||||||
|
const newMembership = await findMembershipWithOrganization(userId);
|
||||||
|
if (!newMembership) {
|
||||||
|
throw new Error("Failed to create default organization");
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMembership;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { test, describe, mock, beforeEach, afterEach, expect } from "bun:test";
|
import { test, describe, beforeEach, expect } from "bun:test";
|
||||||
import { convertLegacyUserOnFirstLogin } from "../convert-legacy-user";
|
import { convertLegacyUserOnFirstLogin } from "../convert-legacy-user";
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { usersTable, account, organization, member } from "~/server/db/schema";
|
import { usersTable, account, organization, member } from "~/server/db/schema";
|
||||||
import type { AuthMiddlewareContext } from "../../auth";
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
|
|
||||||
describe("convertLegacyUserOnFirstLogin", () => {
|
describe("convertLegacyUserOnFirstLogin", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
@ -12,10 +12,6 @@ describe("convertLegacyUserOnFirstLogin", () => {
|
||||||
await db.delete(usersTable);
|
await db.delete(usersTable);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
const createContext = (path: string, body: Record<string, string>) => ({ path, body }) as AuthMiddlewareContext;
|
const createContext = (path: string, body: Record<string, string>) => ({ path, body }) as AuthMiddlewareContext;
|
||||||
|
|
||||||
test("should return early for non-sign-in paths", async () => {
|
test("should return early for non-sign-in paths", async () => {
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import { isSsoCallbackRequest, requireSsoInvitation } from "../require-sso-invitation";
|
||||||
|
|
||||||
|
function createMockContext(path: string, params: Record<string, string> = {}): GenericEndpointContext {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
body: {},
|
||||||
|
query: {},
|
||||||
|
headers: new Headers(),
|
||||||
|
request: new Request(`http://test.local${path}`),
|
||||||
|
params,
|
||||||
|
method: "POST",
|
||||||
|
context: {} as GenericEndpointContext["context"],
|
||||||
|
} as GenericEndpointContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockSsoCallbackContext(providerId: string): GenericEndpointContext {
|
||||||
|
return createMockContext(`/sso/callback/${providerId}`, { providerId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
return Bun.randomUUIDv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomSlug(prefix: string) {
|
||||||
|
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSsoProvider(providerId: string) {
|
||||||
|
const inviterId = randomId();
|
||||||
|
const organizationId = randomId();
|
||||||
|
|
||||||
|
await db.insert(usersTable).values({
|
||||||
|
id: inviterId,
|
||||||
|
username: randomSlug("inviter"),
|
||||||
|
email: `${randomSlug("inviter")}@example.com`,
|
||||||
|
name: "Inviter",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id: organizationId,
|
||||||
|
name: "Acme",
|
||||||
|
slug: randomSlug("acme"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId,
|
||||||
|
organizationId,
|
||||||
|
userId: inviterId,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { inviterId, organizationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("requireSsoInvitation", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.delete(member);
|
||||||
|
await db.delete(account);
|
||||||
|
await db.delete(invitation);
|
||||||
|
await db.delete(ssoProvider);
|
||||||
|
await db.delete(organization);
|
||||||
|
await db.delete(usersTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when context is null", async () => {
|
||||||
|
await createSsoProvider("oidc-acme");
|
||||||
|
|
||||||
|
expect(requireSsoInvitation("user@example.com", null)).rejects.toThrow("Missing SSO context");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when request is not an SSO callback", async () => {
|
||||||
|
await createSsoProvider("oidc-acme");
|
||||||
|
|
||||||
|
const ctx = createMockContext("/sign-up/email");
|
||||||
|
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("Missing providerId");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects whether current request is an SSO callback", async () => {
|
||||||
|
expect(isSsoCallbackRequest(null)).toBe(false);
|
||||||
|
|
||||||
|
const nonSsoResult = isSsoCallbackRequest(createMockContext("/sign-up/email"));
|
||||||
|
expect(nonSsoResult).toBe(false);
|
||||||
|
|
||||||
|
const ssoResult = isSsoCallbackRequest(createMockSsoCallbackContext("oidc-acme"));
|
||||||
|
expect(ssoResult).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks SSO callback when no pending invitation exists", async () => {
|
||||||
|
await createSsoProvider("oidc-acme");
|
||||||
|
|
||||||
|
const ctx = createMockSsoCallbackContext("oidc-acme");
|
||||||
|
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks SSO callback when invitation is expired", async () => {
|
||||||
|
const { inviterId, organizationId } = await createSsoProvider("oidc-acme");
|
||||||
|
|
||||||
|
await db.insert(invitation).values({
|
||||||
|
id: randomId(),
|
||||||
|
organizationId,
|
||||||
|
email: "user@example.com",
|
||||||
|
role: "member",
|
||||||
|
status: "pending",
|
||||||
|
expiresAt: new Date(Date.now() - 1_000),
|
||||||
|
createdAt: new Date(),
|
||||||
|
inviterId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = createMockSsoCallbackContext("oidc-acme");
|
||||||
|
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows SSO callback when a matching pending invitation exists", async () => {
|
||||||
|
const { inviterId, organizationId } = await createSsoProvider("oidc-acme");
|
||||||
|
|
||||||
|
await db.insert(invitation).values({
|
||||||
|
id: randomId(),
|
||||||
|
organizationId,
|
||||||
|
email: "user@example.com",
|
||||||
|
role: "member",
|
||||||
|
status: "pending",
|
||||||
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||||
|
createdAt: new Date(),
|
||||||
|
inviterId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = createMockSsoCallbackContext("oidc-acme");
|
||||||
|
expect(requireSsoInvitation(" USER@EXAMPLE.COM ", ctx)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when provider is not registered", async () => {
|
||||||
|
const ctx = createMockSsoCallbackContext("missing-provider");
|
||||||
|
expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("SSO provider not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { account, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import { isSsoCallbackPath, trustSsoProviderForLinking } from "../trust-sso-provider-for-linking";
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
return Bun.randomUUIDv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomSlug(prefix: string) {
|
||||||
|
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockContext(options: {
|
||||||
|
path: string;
|
||||||
|
method?: string;
|
||||||
|
params?: Record<string, string>;
|
||||||
|
trustedProviders?: string[];
|
||||||
|
enabled?: boolean;
|
||||||
|
}): GenericEndpointContext {
|
||||||
|
const { path, method = "GET", params = {}, trustedProviders = [], enabled = true } = options;
|
||||||
|
|
||||||
|
const accountLinking = {
|
||||||
|
enabled,
|
||||||
|
trustedProviders,
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
options: {
|
||||||
|
account: {
|
||||||
|
accountLinking,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
body: {},
|
||||||
|
query: {},
|
||||||
|
headers: new Headers(),
|
||||||
|
request: new Request(`http://test.local${path}`, { method }),
|
||||||
|
params,
|
||||||
|
method,
|
||||||
|
context: context as GenericEndpointContext["context"],
|
||||||
|
} as GenericEndpointContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSsoProviderRecord(
|
||||||
|
providerId: string,
|
||||||
|
autoLinkMatchingEmails: boolean,
|
||||||
|
options: { organizationId?: string; userId?: string } = {},
|
||||||
|
) {
|
||||||
|
const userId = options.userId ?? randomId();
|
||||||
|
const organizationId = options.organizationId ?? randomId();
|
||||||
|
|
||||||
|
if (!options.userId) {
|
||||||
|
await db.insert(usersTable).values({
|
||||||
|
id: userId,
|
||||||
|
username: randomSlug("inviter"),
|
||||||
|
email: `${randomSlug("inviter")}@example.com`,
|
||||||
|
name: "Inviter",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.organizationId) {
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id: organizationId,
|
||||||
|
name: "Acme",
|
||||||
|
slug: randomSlug("acme"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId,
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
autoLinkMatchingEmails,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organizationId, userId };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("isSsoCallbackPath", () => {
|
||||||
|
test("detects OIDC callback paths", () => {
|
||||||
|
expect(isSsoCallbackPath("/sso/callback/pocket-id")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores non-callback paths", () => {
|
||||||
|
expect(isSsoCallbackPath("/sso/register")).toBe(false);
|
||||||
|
expect(isSsoCallbackPath("/sign-in/email")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("trustSsoProviderForLinking", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.delete(member);
|
||||||
|
await db.delete(account);
|
||||||
|
await db.delete(ssoProvider);
|
||||||
|
await db.delete(organization);
|
||||||
|
await db.delete(usersTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds callback provider to trusted providers", async () => {
|
||||||
|
await createSsoProviderRecord("pocket-id", true);
|
||||||
|
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/pocket-id",
|
||||||
|
params: { providerId: "pocket-id" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toContain("pocket-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not trust providers with disabled auto-linking", async () => {
|
||||||
|
await createSsoProviderRecord("pocket-id", false);
|
||||||
|
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/pocket-id",
|
||||||
|
params: { providerId: "pocket-id" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaces stale trusted providers with database state", async () => {
|
||||||
|
await createSsoProviderRecord("pocket-id", true);
|
||||||
|
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/pocket-id",
|
||||||
|
params: { providerId: "pocket-id" },
|
||||||
|
trustedProviders: ["stale-provider"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual(["pocket-id"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not trust unknown providers", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/missing-provider",
|
||||||
|
params: { providerId: "missing-provider" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not duplicate an existing provider", async () => {
|
||||||
|
await createSsoProviderRecord("pocket-id", true);
|
||||||
|
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/pocket-id",
|
||||||
|
params: { providerId: "pocket-id" },
|
||||||
|
trustedProviders: ["pocket-id"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual(["pocket-id"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removes provider from trusted providers when auto-linking is disabled", async () => {
|
||||||
|
await createSsoProviderRecord("pocket-id", true);
|
||||||
|
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/pocket-id",
|
||||||
|
params: { providerId: "pocket-id" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual(["pocket-id"]);
|
||||||
|
|
||||||
|
await db.update(ssoProvider).set({ autoLinkMatchingEmails: false }).where(eq(ssoProvider.providerId, "pocket-id"));
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does nothing when account linking is disabled", async () => {
|
||||||
|
await createSsoProviderRecord("pocket-id", true);
|
||||||
|
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/pocket-id",
|
||||||
|
params: { providerId: "pocket-id" },
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does nothing when provider id cannot be extracted", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
path: "/sso/callback/",
|
||||||
|
params: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
|
||||||
|
expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
|
import { validateSsoCallbackUrls } from "../validate-sso-callback-urls";
|
||||||
|
|
||||||
|
function createContext(
|
||||||
|
path: string,
|
||||||
|
body: Record<string, unknown> = {},
|
||||||
|
query: Record<string, unknown> = {},
|
||||||
|
): AuthMiddlewareContext {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
body,
|
||||||
|
query,
|
||||||
|
headers: new Headers(),
|
||||||
|
request: new Request(`http://localhost:3000${path}`),
|
||||||
|
params: {},
|
||||||
|
method: "POST",
|
||||||
|
context: {} as AuthMiddlewareContext["context"],
|
||||||
|
} as AuthMiddlewareContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("validateSsoCallbackUrls", () => {
|
||||||
|
test("accepts relative paths for every callback field", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", {
|
||||||
|
callbackURL: "/login",
|
||||||
|
errorCallbackURL: "/login/error",
|
||||||
|
newUserCallbackURL: "/download-recovery-key",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects https://evil.example", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", { callbackURL: "https://evil.example" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects //evil.example", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", { callbackURL: "//evil.example" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects /sso/callback/foo", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", { callbackURL: "/sso/callback/foo" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects /sso/saml2/foo", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", { callbackURL: "/sso/saml2/foo" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malicious query callback fields", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", {}, { errorCallbackURL: "https://evil.example" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("errorCallbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malicious newUserCallbackURL", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", { newUserCallbackURL: "https://evil.example" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("newUserCallbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips validation outside SSO sign-in endpoint", async () => {
|
||||||
|
const ctx = createContext("/sign-in/email", { callbackURL: "https://evil.example" });
|
||||||
|
|
||||||
|
expect(validateSsoCallbackUrls(ctx)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
|
import { validateSsoProviderId } from "../validate-sso-provider-id";
|
||||||
|
|
||||||
|
function createContext(path: string, body: Record<string, unknown> = {}): AuthMiddlewareContext {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
body,
|
||||||
|
query: {},
|
||||||
|
headers: new Headers(),
|
||||||
|
request: new Request(`http://localhost:3000${path}`),
|
||||||
|
params: {},
|
||||||
|
method: "POST",
|
||||||
|
context: {} as AuthMiddlewareContext["context"],
|
||||||
|
} as AuthMiddlewareContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("validateSsoProviderId", () => {
|
||||||
|
test("allows non-reserved provider id", async () => {
|
||||||
|
const ctx = createContext("/sso/register", { providerId: "acme-oidc" });
|
||||||
|
|
||||||
|
expect(validateSsoProviderId(ctx)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects reserved credential provider id", async () => {
|
||||||
|
const ctx = createContext("/sso/register", { providerId: "credential" });
|
||||||
|
|
||||||
|
expect(validateSsoProviderId(ctx)).rejects.toThrow("reserved");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects reserved credentials provider id case-insensitively", async () => {
|
||||||
|
const ctx = createContext("/sso/register", { providerId: " Credential " });
|
||||||
|
|
||||||
|
expect(validateSsoProviderId(ctx)).rejects.toThrow("reserved");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips validation outside register endpoint", async () => {
|
||||||
|
const ctx = createContext("/sign-in/sso", { providerId: "credential" });
|
||||||
|
|
||||||
|
expect(validateSsoProviderId(ctx)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -2,7 +2,7 @@ import { hashPassword } from "better-auth/crypto";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { account, usersTable, member, organization } from "~/server/db/schema";
|
import { account, usersTable, member, organization } from "~/server/db/schema";
|
||||||
import type { AuthMiddlewareContext } from "../auth";
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
import { UnauthorizedError } from "http-errors-enhanced";
|
import { UnauthorizedError } from "http-errors-enhanced";
|
||||||
import { cryptoUtils } from "~/server/utils/crypto";
|
import { cryptoUtils } from "~/server/utils/crypto";
|
||||||
import { normalizeUsername } from "~/lib/username";
|
import { normalizeUsername } from "~/lib/username";
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import type { AuthMiddlewareContext } from "../auth";
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
import { logger } from "~/server/utils/logger";
|
import { logger } from "~/server/utils/logger";
|
||||||
import { ForbiddenError } from "http-errors-enhanced";
|
import { ForbiddenError } from "http-errors-enhanced";
|
||||||
import { REGISTRATION_ENABLED_KEY } from "~/server/core/constants";
|
import { REGISTRATION_ENABLED_KEY } from "~/server/core/constants";
|
||||||
|
|
||||||
export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => {
|
export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => {
|
||||||
const { path } = ctx;
|
const { path } = ctx;
|
||||||
const existingUser = await db.query.usersTable.findFirst();
|
|
||||||
|
|
||||||
if (path !== "/sign-up/email") {
|
if (path !== "/sign-up/email") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingUser = await db.query.usersTable.findFirst();
|
||||||
|
|
||||||
const result = await db.query.appMetadataTable.findFirst({
|
const result = await db.query.appMetadataTable.findFirst({
|
||||||
where: { key: REGISTRATION_ENABLED_KEY },
|
where: { key: REGISTRATION_ENABLED_KEY },
|
||||||
});
|
});
|
||||||
52
app/server/lib/auth/middlewares/require-sso-invitation.ts
Normal file
52
app/server/lib/auth/middlewares/require-sso-invitation.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { APIError } from "better-auth/api";
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { logger } from "~/server/utils/logger";
|
||||||
|
import { extractProviderIdFromContext, extractProviderIdFromUrl, normalizeEmail } from "../utils/sso-context";
|
||||||
|
|
||||||
|
export function isSsoCallbackRequest(ctx: GenericEndpointContext | null) {
|
||||||
|
if (!ctx?.request?.url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractProviderIdFromUrl(ctx.request.url) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requireSsoInvitation = async (userEmail: string, ctx: GenericEndpointContext | null) => {
|
||||||
|
if (!ctx) {
|
||||||
|
throw new APIError("BAD_REQUEST", { message: "Missing SSO context" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerId = extractProviderIdFromContext(ctx);
|
||||||
|
if (!providerId) {
|
||||||
|
throw new APIError("BAD_REQUEST", { message: "Missing providerId in context" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await db.query.ssoProvider.findFirst({ where: { providerId } });
|
||||||
|
if (!provider) {
|
||||||
|
throw new APIError("NOT_FOUND", { message: "SSO provider not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedEmail = normalizeEmail(userEmail);
|
||||||
|
logger.debug("Checking for pending invitations", { organizationId: provider.organizationId });
|
||||||
|
|
||||||
|
const pendingInvitation = await db.query.invitation.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{ organizationId: provider.organizationId },
|
||||||
|
{ status: "pending" },
|
||||||
|
{ expiresAt: { gt: new Date() } },
|
||||||
|
{ email: normalizedEmail },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Pending invitation result", { found: !!pendingInvitation, invitationId: pendingInvitation?.id });
|
||||||
|
|
||||||
|
if (!pendingInvitation) {
|
||||||
|
throw new APIError("FORBIDDEN", {
|
||||||
|
message: "Access denied. You must be invited to this organization before you can sign in with SSO.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { extractProviderIdFromContext } from "../utils/sso-context";
|
||||||
|
|
||||||
|
export function isSsoCallbackPath(path?: string): boolean {
|
||||||
|
if (!path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.startsWith("/sso/callback/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function trustSsoProviderForLinking(ctx: GenericEndpointContext): Promise<void> {
|
||||||
|
const providerId = extractProviderIdFromContext(ctx);
|
||||||
|
|
||||||
|
if (!providerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountLinking = ctx.context.options.account?.accountLinking;
|
||||||
|
|
||||||
|
if (!accountLinking || accountLinking.enabled === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await db.query.ssoProvider.findFirst({
|
||||||
|
columns: { organizationId: true },
|
||||||
|
where: { providerId },
|
||||||
|
});
|
||||||
|
if (!provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoLinkingProviders = await db.query.ssoProvider.findMany({
|
||||||
|
columns: { providerId: true },
|
||||||
|
where: {
|
||||||
|
organizationId: provider.organizationId,
|
||||||
|
autoLinkMatchingEmails: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
accountLinking.trustedProviders = autoLinkingProviders.map((entry) => entry.providerId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { APIError } from "better-auth/api";
|
||||||
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
|
|
||||||
|
function isValidCallbackPath(value: string): boolean {
|
||||||
|
if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith("/sso/callback/") || value.startsWith("/sso/saml2/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateSsoCallbackUrls = async (ctx: AuthMiddlewareContext) => {
|
||||||
|
if (ctx.path !== "/sign-in/sso") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = [ctx.body, ctx.query].filter((s) => s && typeof s === "object");
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
const payload = source as Record<string, unknown>;
|
||||||
|
|
||||||
|
for (const field of ["callbackURL", "errorCallbackURL", "newUserCallbackURL"]) {
|
||||||
|
const value = payload[field];
|
||||||
|
|
||||||
|
if (value !== undefined && (typeof value !== "string" || !isValidCallbackPath(value))) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: `Invalid ${field}. Only relative paths like /login are allowed.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
25
app/server/lib/auth/middlewares/validate-sso-provider-id.ts
Normal file
25
app/server/lib/auth/middlewares/validate-sso-provider-id.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { APIError } from "better-auth/api";
|
||||||
|
import type { AuthMiddlewareContext } from "~/server/lib/auth";
|
||||||
|
import { isReservedSsoProviderId } from "../utils/sso-provider-id";
|
||||||
|
|
||||||
|
export const validateSsoProviderId = async (ctx: AuthMiddlewareContext) => {
|
||||||
|
if (ctx.path !== "/sso/register") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.body || typeof ctx.body !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerId = (ctx.body as Record<string, unknown>).providerId;
|
||||||
|
|
||||||
|
if (typeof providerId !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReservedSsoProviderId(providerId)) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: `Invalid providerId. '${providerId}' is reserved and cannot be used for SSO providers.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
21
app/server/lib/auth/plugins/sso-trusted-provider-linking.ts
Normal file
21
app/server/lib/auth/plugins/sso-trusted-provider-linking.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { BetterAuthPlugin } from "better-auth";
|
||||||
|
import { createAuthMiddleware } from "better-auth/api";
|
||||||
|
import { isSsoCallbackPath, trustSsoProviderForLinking } from "../middlewares/trust-sso-provider-for-linking";
|
||||||
|
|
||||||
|
export function ssoTrustedProviderLinkingPlugin(): BetterAuthPlugin {
|
||||||
|
return {
|
||||||
|
id: "sso-trusted-provider-linking",
|
||||||
|
hooks: {
|
||||||
|
before: [
|
||||||
|
{
|
||||||
|
matcher(context) {
|
||||||
|
return isSsoCallbackPath(context.path);
|
||||||
|
},
|
||||||
|
handler: createAuthMiddleware(async (ctx) => {
|
||||||
|
await trustSsoProviderForLinking(ctx);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
31
app/server/lib/auth/utils/sso-context.ts
Normal file
31
app/server/lib/auth/utils/sso-context.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import type { GenericEndpointContext } from "@better-auth/core";
|
||||||
|
|
||||||
|
export function normalizeEmail(email: string): string {
|
||||||
|
return email.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractProviderIdFromUrl(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(url, "http://localhost").pathname;
|
||||||
|
const match = pathname.match(/\/sso\/(?:saml2\/)?callback\/([^/]+)$/);
|
||||||
|
return match?.[1] ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractProviderIdFromContext(ctx?: GenericEndpointContext | null) {
|
||||||
|
if (!ctx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.params?.providerId) {
|
||||||
|
return ctx.params.providerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.request?.url) {
|
||||||
|
return extractProviderIdFromUrl(ctx.request.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
9
app/server/lib/auth/utils/sso-provider-id.ts
Normal file
9
app/server/lib/auth/utils/sso-provider-id.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
const RESERVED_SSO_PROVIDER_IDS = new Set(["credential"]);
|
||||||
|
|
||||||
|
export function normalizeSsoProviderId(providerId: string): string {
|
||||||
|
return providerId.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReservedSsoProviderId(providerId: string): boolean {
|
||||||
|
return RESERVED_SSO_PROVIDER_IDS.has(normalizeSsoProviderId(providerId));
|
||||||
|
}
|
||||||
31
app/server/lib/functions/organization-context.ts
Normal file
31
app/server/lib/functions/organization-context.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { auth } from "../auth";
|
||||||
|
import { getRequest } from "@tanstack/react-start/server";
|
||||||
|
|
||||||
|
export const getOrganizationContext = createServerFn({ method: "GET" }).handler(async () => {
|
||||||
|
const request = getRequest();
|
||||||
|
|
||||||
|
const [data, session] = await Promise.all([
|
||||||
|
auth.api.listOrganizations({
|
||||||
|
headers: request.headers,
|
||||||
|
}),
|
||||||
|
auth.api.getSession({ headers: request.headers }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeOrganizationId = session?.session?.activeOrganizationId;
|
||||||
|
const activeOrganization = data.find((org) => org.id === activeOrganizationId);
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
throw new Error("No organizations found for user");
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await auth.api.getActiveMember({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizations: data,
|
||||||
|
activeOrganization: activeOrganization || data[0],
|
||||||
|
activeMember: member,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import { authService } from "../auth.service";
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
return Bun.randomUUIDv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomSlug(prefix: string) {
|
||||||
|
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser(email: string) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
await db.insert(usersTable).values({
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name: email.split("@")[0],
|
||||||
|
username: randomSlug("user"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrganization(name: string) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
slug: randomSlug("org"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("authService.deleteSsoProvider", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.delete(member);
|
||||||
|
await db.delete(account);
|
||||||
|
await db.delete(invitation);
|
||||||
|
await db.delete(ssoProvider);
|
||||||
|
await db.delete(organization);
|
||||||
|
await db.delete(usersTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not delete accounts when provider belongs to another organization", async () => {
|
||||||
|
const orgA = await createOrganization("Org A");
|
||||||
|
const orgB = await createOrganization("Org B");
|
||||||
|
const providerOwner = await createUser(`${randomSlug("owner")}@example.com`);
|
||||||
|
const accountUser = await createUser(`${randomSlug("member")}@example.com`);
|
||||||
|
|
||||||
|
const providerId = `oidc-${randomSlug("provider")}`;
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId,
|
||||||
|
organizationId: orgB,
|
||||||
|
userId: providerOwner,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(account).values({
|
||||||
|
id: randomId(),
|
||||||
|
accountId: randomSlug("acct"),
|
||||||
|
providerId,
|
||||||
|
userId: accountUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleted = await authService.deleteSsoProvider(providerId, orgA);
|
||||||
|
|
||||||
|
expect(deleted).toBe(false);
|
||||||
|
|
||||||
|
const remainingProvider = await db.query.ssoProvider.findFirst({
|
||||||
|
where: { providerId },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
const remainingAccounts = await db.query.account.findMany({
|
||||||
|
where: { providerId },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(remainingProvider).not.toBeUndefined();
|
||||||
|
expect(remainingAccounts).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deletes provider and linked accounts in the active organization", async () => {
|
||||||
|
const org = await createOrganization("Org A");
|
||||||
|
const providerOwner = await createUser(`${randomSlug("owner")}@example.com`);
|
||||||
|
const accountUserA = await createUser(`${randomSlug("member-a")}@example.com`);
|
||||||
|
const accountUserB = await createUser(`${randomSlug("member-b")}@example.com`);
|
||||||
|
|
||||||
|
const providerId = `oidc-${randomSlug("provider")}`;
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId,
|
||||||
|
organizationId: org,
|
||||||
|
userId: providerOwner,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(account).values([
|
||||||
|
{
|
||||||
|
id: randomId(),
|
||||||
|
accountId: randomSlug("acct-a"),
|
||||||
|
providerId,
|
||||||
|
userId: accountUserA,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: randomId(),
|
||||||
|
accountId: randomSlug("acct-b"),
|
||||||
|
providerId,
|
||||||
|
userId: accountUserB,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deleted = await authService.deleteSsoProvider(providerId, org);
|
||||||
|
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
|
||||||
|
const remainingProvider = await db.query.ssoProvider.findFirst({
|
||||||
|
where: { providerId },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
const remainingAccounts = await db.query.account.findMany({
|
||||||
|
where: { providerId },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(remainingProvider).toBeUndefined();
|
||||||
|
expect(remainingAccounts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deleting a reserved provider id never deletes credential accounts", async () => {
|
||||||
|
const org = await createOrganization("Org A");
|
||||||
|
const providerOwner = await createUser(`${randomSlug("owner")}@example.com`);
|
||||||
|
const credentialUser = await createUser(`${randomSlug("credential")}@example.com`);
|
||||||
|
|
||||||
|
await db.insert(ssoProvider).values({
|
||||||
|
id: randomId(),
|
||||||
|
providerId: "credential",
|
||||||
|
organizationId: org,
|
||||||
|
userId: providerOwner,
|
||||||
|
issuer: "https://issuer.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(account).values({
|
||||||
|
id: randomId(),
|
||||||
|
accountId: randomSlug("credential-acct"),
|
||||||
|
providerId: "credential",
|
||||||
|
userId: credentialUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleted = await authService.deleteSsoProvider("credential", org);
|
||||||
|
|
||||||
|
expect(deleted).toBe(true);
|
||||||
|
|
||||||
|
const remainingProvider = await db.query.ssoProvider.findFirst({
|
||||||
|
where: { providerId: "credential" },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
const remainingAccounts = await db.query.account.findMany({
|
||||||
|
where: { providerId: "credential" },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(remainingProvider).toBeUndefined();
|
||||||
|
expect(remainingAccounts).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
94
app/server/modules/auth/__tests__/auth.sso-security.test.ts
Normal file
94
app/server/modules/auth/__tests__/auth.sso-security.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { createApp } from "~/server/app";
|
||||||
|
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
describe("auth SSO sign-in security", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.delete(member);
|
||||||
|
await db.delete(account);
|
||||||
|
await db.delete(invitation);
|
||||||
|
await db.delete(ssoProvider);
|
||||||
|
await db.delete(organization);
|
||||||
|
await db.delete(usersTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malicious callback URL", async () => {
|
||||||
|
const response = await app.request("/api/auth/sign-in/sso", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
providerId: "missing-provider",
|
||||||
|
callbackURL: "https://evil.example",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.code).toContain("CALLBACKURL");
|
||||||
|
expect(body.message).toContain("callbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malicious error callback URL", async () => {
|
||||||
|
const response = await app.request("/api/auth/sign-in/sso", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
providerId: "missing-provider",
|
||||||
|
callbackURL: "/login",
|
||||||
|
errorCallbackURL: "https://evil.example",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.code).toContain("ERRORCALLBACKURL");
|
||||||
|
expect(body.message).toContain("errorCallbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malicious new user callback URL", async () => {
|
||||||
|
const response = await app.request("/api/auth/sign-in/sso", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
providerId: "missing-provider",
|
||||||
|
callbackURL: "/login",
|
||||||
|
newUserCallbackURL: "https://evil.example",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.code).toContain("NEWUSERCALLBACKURL");
|
||||||
|
expect(body.message).toContain("newUserCallbackURL");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows relative callback URL to continue normal flow", async () => {
|
||||||
|
const response = await app.request("/api/auth/sign-in/sso", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
providerId: "missing-provider",
|
||||||
|
callbackURL: "/login",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body.code).toBe("NO_PROVIDER_FOUND_FOR_THE_ISSUER");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,13 +1,152 @@
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { type GetStatusDto, getStatusDto, getUserDeletionImpactDto, type UserDeletionImpactDto } from "./auth.dto";
|
import { validator } from "hono-openapi";
|
||||||
|
import {
|
||||||
|
type GetStatusDto,
|
||||||
|
getStatusDto,
|
||||||
|
getUserDeletionImpactDto,
|
||||||
|
type UserDeletionImpactDto,
|
||||||
|
getPublicSsoProvidersDto,
|
||||||
|
type PublicSsoProvidersDto,
|
||||||
|
getSsoSettingsDto,
|
||||||
|
type SsoSettingsDto,
|
||||||
|
getAdminUsersDto,
|
||||||
|
type AdminUsersDto,
|
||||||
|
deleteSsoProviderDto,
|
||||||
|
deleteSsoInvitationDto,
|
||||||
|
updateSsoProviderAutoLinkingBody,
|
||||||
|
updateSsoProviderAutoLinkingDto,
|
||||||
|
deleteUserAccountDto,
|
||||||
|
} from "./auth.dto";
|
||||||
import { authService } from "./auth.service";
|
import { authService } from "./auth.service";
|
||||||
import { requireAdmin, requireAuth } from "./auth.middleware";
|
import { requireAdmin, requireAuth } from "./auth.middleware";
|
||||||
|
import { auth } from "~/server/lib/auth";
|
||||||
|
|
||||||
export const authController = new Hono()
|
export const authController = new Hono()
|
||||||
.get("/status", getStatusDto, async (c) => {
|
.get("/status", getStatusDto, async (c) => {
|
||||||
const hasUsers = await authService.hasUsers();
|
const hasUsers = await authService.hasUsers();
|
||||||
return c.json<GetStatusDto>({ hasUsers });
|
return c.json<GetStatusDto>({ hasUsers });
|
||||||
})
|
})
|
||||||
|
.get("/sso-providers", getPublicSsoProvidersDto, async (c) => {
|
||||||
|
const providers = await authService.getPublicSsoProviders();
|
||||||
|
return c.json<PublicSsoProvidersDto>(providers);
|
||||||
|
})
|
||||||
|
.get("/sso-settings", requireAuth, requireAdmin, getSsoSettingsDto, async (c) => {
|
||||||
|
const headers = c.req.raw.headers;
|
||||||
|
const activeOrganizationId = c.get("organizationId");
|
||||||
|
|
||||||
|
if (!activeOrganizationId) {
|
||||||
|
return c.json<SsoSettingsDto>({ providers: [], invitations: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [providersData, invitationsData, autoLinkingSettings] = await Promise.all([
|
||||||
|
auth.api.listSSOProviders({ headers }),
|
||||||
|
auth.api.listInvitations({ headers, query: { organizationId: activeOrganizationId } }),
|
||||||
|
authService.getSsoProviderAutoLinkingSettings(activeOrganizationId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return c.json<SsoSettingsDto>({
|
||||||
|
providers: providersData.providers.map((provider) => ({
|
||||||
|
providerId: provider.providerId,
|
||||||
|
type: provider.type,
|
||||||
|
issuer: provider.issuer,
|
||||||
|
domain: provider.domain,
|
||||||
|
autoLinkMatchingEmails: autoLinkingSettings[provider.providerId] ?? false,
|
||||||
|
organizationId: provider.organizationId,
|
||||||
|
})),
|
||||||
|
invitations: invitationsData.map((invitation) => ({
|
||||||
|
id: invitation.id,
|
||||||
|
email: invitation.email,
|
||||||
|
role: invitation.role,
|
||||||
|
status: invitation.status,
|
||||||
|
expiresAt: invitation.expiresAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.delete("/sso-providers/:providerId", requireAuth, requireAdmin, deleteSsoProviderDto, async (c) => {
|
||||||
|
const providerId = c.req.param("providerId");
|
||||||
|
const organizationId = c.get("organizationId");
|
||||||
|
|
||||||
|
const deleted = await authService.deleteSsoProvider(providerId, organizationId);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return c.json({ message: "Provider not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ success: true });
|
||||||
|
})
|
||||||
|
.patch(
|
||||||
|
"/sso-providers/:providerId/auto-linking",
|
||||||
|
requireAuth,
|
||||||
|
requireAdmin,
|
||||||
|
updateSsoProviderAutoLinkingDto,
|
||||||
|
validator("json", updateSsoProviderAutoLinkingBody),
|
||||||
|
async (c) => {
|
||||||
|
const providerId = c.req.param("providerId");
|
||||||
|
const organizationId = c.get("organizationId");
|
||||||
|
const { enabled } = c.req.valid("json");
|
||||||
|
|
||||||
|
const updated = await authService.updateSsoProviderAutoLinking(providerId, organizationId, enabled);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return c.json({ message: "Provider not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ success: true });
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.delete("/sso-invitations/:invitationId", requireAuth, requireAdmin, deleteSsoInvitationDto, async (c) => {
|
||||||
|
const invitationId = c.req.param("invitationId");
|
||||||
|
const organizationId = c.get("organizationId");
|
||||||
|
|
||||||
|
const invitation = await authService.getSsoInvitationById(invitationId);
|
||||||
|
if (!invitation || invitation.organizationId !== organizationId) {
|
||||||
|
return c.json({ message: "Invitation not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
await authService.deleteSsoInvitation(invitationId);
|
||||||
|
|
||||||
|
return c.json({ success: true });
|
||||||
|
})
|
||||||
|
.get("/admin-users", requireAuth, requireAdmin, getAdminUsersDto, async (c) => {
|
||||||
|
const headers = c.req.raw.headers;
|
||||||
|
|
||||||
|
const usersData = await auth.api.listUsers({
|
||||||
|
headers,
|
||||||
|
query: { limit: 100 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userIds = usersData.users.map((u) => u.id);
|
||||||
|
const accountsByUser = await authService.getUserAccounts(userIds);
|
||||||
|
|
||||||
|
return c.json<AdminUsersDto>({
|
||||||
|
users: usersData.users.map((adminUser) => ({
|
||||||
|
id: adminUser.id,
|
||||||
|
name: adminUser.name,
|
||||||
|
email: adminUser.email,
|
||||||
|
role: adminUser.role ?? "user",
|
||||||
|
banned: Boolean(adminUser.banned),
|
||||||
|
accounts: accountsByUser[adminUser.id] ?? [],
|
||||||
|
})),
|
||||||
|
total: usersData.total,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.delete("/admin-users/:userId/accounts/:accountId", requireAuth, requireAdmin, deleteUserAccountDto, async (c) => {
|
||||||
|
const userId = c.req.param("userId");
|
||||||
|
const accountId = c.req.param("accountId");
|
||||||
|
const organizationId = c.get("organizationId");
|
||||||
|
|
||||||
|
const result = await authService.deleteUserAccount(userId, accountId, organizationId);
|
||||||
|
|
||||||
|
if (result.forbidden) {
|
||||||
|
return c.json({ message: "User is not a member of this organization" }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.lastAccount) {
|
||||||
|
return c.json({ message: "Cannot delete the last account of a user" }, 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ success: true });
|
||||||
|
})
|
||||||
.get("/deletion-impact/:userId", requireAuth, requireAdmin, getUserDeletionImpactDto, async (c) => {
|
.get("/deletion-impact/:userId", requireAuth, requireAdmin, getUserDeletionImpactDto, async (c) => {
|
||||||
const userId = c.req.param("userId");
|
const userId = c.req.param("userId");
|
||||||
const impact = await authService.getUserDeletionImpact(userId);
|
const impact = await authService.getUserDeletionImpact(userId);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,102 @@ const statusResponseSchema = type({
|
||||||
hasUsers: "boolean",
|
hasUsers: "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const publicSsoProvidersDto = type({
|
||||||
|
providers: type({
|
||||||
|
providerId: "string",
|
||||||
|
organizationSlug: "string",
|
||||||
|
})
|
||||||
|
.onUndeclaredKey("delete")
|
||||||
|
.array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PublicSsoProvidersDto = typeof publicSsoProvidersDto.infer;
|
||||||
|
|
||||||
|
export const getPublicSsoProvidersDto = describeRoute({
|
||||||
|
description: "Get public SSO providers for the instance",
|
||||||
|
operationId: "getPublicSsoProviders",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of public SSO providers",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(publicSsoProvidersDto),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ssoSettingsResponse = type({
|
||||||
|
providers: type({
|
||||||
|
providerId: "string",
|
||||||
|
type: "string",
|
||||||
|
issuer: "string",
|
||||||
|
domain: "string",
|
||||||
|
autoLinkMatchingEmails: "boolean",
|
||||||
|
organizationId: "string | null",
|
||||||
|
}).array(),
|
||||||
|
invitations: type({
|
||||||
|
id: "string",
|
||||||
|
email: "string",
|
||||||
|
role: "string",
|
||||||
|
status: "string",
|
||||||
|
expiresAt: "string",
|
||||||
|
}).array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SsoSettingsDto = typeof ssoSettingsResponse.infer;
|
||||||
|
|
||||||
|
export const getSsoSettingsDto = describeRoute({
|
||||||
|
description: "Get SSO providers and invitations for the active organization",
|
||||||
|
operationId: "getSsoSettings",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "SSO settings for the active organization",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(ssoSettingsResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const adminUsersResponse = type({
|
||||||
|
users: type({
|
||||||
|
id: "string",
|
||||||
|
name: "string | null",
|
||||||
|
email: "string",
|
||||||
|
role: "string",
|
||||||
|
banned: "boolean",
|
||||||
|
accounts: type({
|
||||||
|
id: "string",
|
||||||
|
providerId: "string",
|
||||||
|
}).array(),
|
||||||
|
}).array(),
|
||||||
|
total: "number",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AdminUsersDto = typeof adminUsersResponse.infer;
|
||||||
|
|
||||||
|
export const getAdminUsersDto = describeRoute({
|
||||||
|
description: "List admin users for settings management",
|
||||||
|
operationId: "getAdminUsers",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of users with roles and status",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(adminUsersResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const getStatusDto = describeRoute({
|
export const getStatusDto = describeRoute({
|
||||||
description: "Get authentication system status",
|
description: "Get authentication system status",
|
||||||
operationId: "getStatus",
|
operationId: "getStatus",
|
||||||
|
|
@ -52,3 +148,72 @@ export const getUserDeletionImpactDto = describeRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deleteSsoProviderDto = describeRoute({
|
||||||
|
description: "Delete an SSO provider",
|
||||||
|
operationId: "deleteSsoProvider",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "SSO provider deleted successfully",
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: "Provider not found",
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
description: "Forbidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteSsoInvitationDto = describeRoute({
|
||||||
|
description: "Delete an SSO invitation",
|
||||||
|
operationId: "deleteSsoInvitation",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "SSO invitation deleted successfully",
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
description: "Forbidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteUserAccountDto = describeRoute({
|
||||||
|
description: "Delete an account linked to a user",
|
||||||
|
operationId: "deleteUserAccount",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Account deleted successfully",
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
description: "Forbidden",
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
description: "Cannot delete the last account",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSsoProviderAutoLinkingBody = type({
|
||||||
|
enabled: "boolean",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSsoProviderAutoLinkingDto = describeRoute({
|
||||||
|
description: "Update whether SSO sign-in can auto-link existing accounts by email",
|
||||||
|
operationId: "updateSsoProviderAutoLinking",
|
||||||
|
tags: ["Auth"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "SSO provider auto-linking setting updated successfully",
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
description: "Forbidden",
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: "Provider not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ declare module "hono" {
|
||||||
interface ContextVariableMap {
|
interface ContextVariableMap {
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
hasDownloadedResticPassword: boolean;
|
hasDownloadedResticPassword: boolean;
|
||||||
role?: string | null | undefined;
|
role?: string | null | undefined;
|
||||||
|
|
|
||||||
|
|
@ -6,23 +6,42 @@ import {
|
||||||
volumesTable,
|
volumesTable,
|
||||||
repositoriesTable,
|
repositoriesTable,
|
||||||
backupSchedulesTable,
|
backupSchedulesTable,
|
||||||
|
ssoProvider,
|
||||||
|
account,
|
||||||
|
invitation,
|
||||||
} from "../../db/schema";
|
} from "../../db/schema";
|
||||||
import { eq, ne, and, count, inArray } from "drizzle-orm";
|
import { eq, ne, and, count, inArray } from "drizzle-orm";
|
||||||
import type { UserDeletionImpactDto } from "./auth.dto";
|
import type { UserDeletionImpactDto } from "./auth.dto";
|
||||||
|
import { isReservedSsoProviderId } from "~/server/lib/auth/utils/sso-provider-id";
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
/**
|
/**
|
||||||
* Check if any users exist in the system
|
* Check if any users exist in the system
|
||||||
*/
|
*/
|
||||||
async hasUsers(): Promise<boolean> {
|
async hasUsers() {
|
||||||
const [user] = await db.select({ id: usersTable.id }).from(usersTable).limit(1);
|
const [user] = await db.select({ id: usersTable.id }).from(usersTable).limit(1);
|
||||||
return !!user;
|
return !!user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get public SSO providers for the instance
|
||||||
|
*/
|
||||||
|
async getPublicSsoProviders() {
|
||||||
|
const providers = await db
|
||||||
|
.select({
|
||||||
|
providerId: ssoProvider.providerId,
|
||||||
|
organizationSlug: organization.slug,
|
||||||
|
})
|
||||||
|
.from(ssoProvider)
|
||||||
|
.innerJoin(organization, eq(ssoProvider.organizationId, organization.id));
|
||||||
|
|
||||||
|
return { providers };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the impact of deleting a user
|
* Get the impact of deleting a user
|
||||||
*/
|
*/
|
||||||
async getUserDeletionImpact(userId: string): Promise<UserDeletionImpactDto> {
|
async getUserDeletionImpact(userId: string) {
|
||||||
const userMemberships = await db.query.member.findMany({
|
const userMemberships = await db.query.member.findMany({
|
||||||
where: {
|
where: {
|
||||||
AND: [{ userId: userId }, { role: "owner" }],
|
AND: [{ userId: userId }, { role: "owner" }],
|
||||||
|
|
@ -77,7 +96,7 @@ export class AuthService {
|
||||||
/**
|
/**
|
||||||
* Cleanup organizations where the user was the sole owner
|
* Cleanup organizations where the user was the sole owner
|
||||||
*/
|
*/
|
||||||
async cleanupUserOrganizations(userId: string): Promise<void> {
|
async cleanupUserOrganizations(userId: string) {
|
||||||
const impact = await this.getUserDeletionImpact(userId);
|
const impact = await this.getUserDeletionImpact(userId);
|
||||||
const orgIds = impact.organizations.map((o) => o.id);
|
const orgIds = impact.organizations.map((o) => o.id);
|
||||||
|
|
||||||
|
|
@ -85,6 +104,137 @@ export class AuthService {
|
||||||
await db.delete(organization).where(inArray(organization.id, orgIds));
|
await db.delete(organization).where(inArray(organization.id, orgIds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an SSO provider and its associated accounts
|
||||||
|
*/
|
||||||
|
async deleteSsoProvider(providerId: string, organizationId: string) {
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
const provider = await tx.query.ssoProvider.findFirst({
|
||||||
|
where: { AND: [{ providerId }, { organizationId }] },
|
||||||
|
columns: { id: true, providerId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReservedSsoProviderId(provider.providerId)) {
|
||||||
|
await tx.delete(ssoProvider).where(eq(ssoProvider.id, provider.id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.delete(account).where(eq(account.providerId, provider.providerId));
|
||||||
|
await tx.delete(ssoProvider).where(eq(ssoProvider.id, provider.id));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get per-provider auto-linking setting for an organization
|
||||||
|
*/
|
||||||
|
async getSsoProviderAutoLinkingSettings(organizationId: string) {
|
||||||
|
const providers = await db.query.ssoProvider.findMany({
|
||||||
|
columns: { providerId: true, autoLinkMatchingEmails: true },
|
||||||
|
where: { organizationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(providers.map((provider) => [provider.providerId, provider.autoLinkMatchingEmails]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update per-provider auto-linking setting
|
||||||
|
*/
|
||||||
|
async updateSsoProviderAutoLinking(providerId: string, organizationId: string, enabled: boolean) {
|
||||||
|
const existingProvider = await db.query.ssoProvider.findFirst({
|
||||||
|
where: { AND: [{ providerId }, { organizationId }] },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingProvider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(ssoProvider)
|
||||||
|
.set({ autoLinkMatchingEmails: enabled })
|
||||||
|
.where(and(eq(ssoProvider.providerId, providerId), eq(ssoProvider.organizationId, organizationId)));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an SSO invitation by ID
|
||||||
|
*/
|
||||||
|
async getSsoInvitationById(invitationId: string) {
|
||||||
|
return db.query.invitation.findFirst({
|
||||||
|
where: { id: invitationId },
|
||||||
|
columns: { id: true, organizationId: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an invitation
|
||||||
|
*/
|
||||||
|
async deleteSsoInvitation(invitationId: string) {
|
||||||
|
await db.delete(invitation).where(eq(invitation.id, invitationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is a member of an organization
|
||||||
|
*/
|
||||||
|
async getUserMembership(userId: string, organizationId: string) {
|
||||||
|
return db.query.member.findFirst({
|
||||||
|
where: { AND: [{ userId }, { organizationId }] },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch accounts for a list of users, keyed by userId
|
||||||
|
*/
|
||||||
|
async getUserAccounts(userIds: string[]) {
|
||||||
|
if (userIds.length === 0) return {};
|
||||||
|
|
||||||
|
const accounts = await db.query.account.findMany({
|
||||||
|
where: { userId: { in: userIds } },
|
||||||
|
columns: { id: true, providerId: true, userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped: Record<string, { id: string; providerId: string }[]> = {};
|
||||||
|
for (const row of accounts) {
|
||||||
|
if (!grouped[row.userId]) {
|
||||||
|
grouped[row.userId] = [];
|
||||||
|
}
|
||||||
|
grouped[row.userId].push({ id: row.id, providerId: row.providerId });
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a single account for a user, refusing if it is the last one
|
||||||
|
*/
|
||||||
|
async deleteUserAccount(userId: string, accountId: string, organizationId: string) {
|
||||||
|
const membership = await this.getUserMembership(userId, organizationId);
|
||||||
|
if (!membership) {
|
||||||
|
return { lastAccount: false, forbidden: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.transaction(async (tx) => {
|
||||||
|
const userAccounts = await tx.query.account.findMany({
|
||||||
|
where: { userId },
|
||||||
|
columns: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userAccounts.length <= 1) {
|
||||||
|
return { lastAccount: true, forbidden: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.delete(account).where(and(eq(account.id, accountId), eq(account.userId, userId)));
|
||||||
|
return { lastAccount: false, forbidden: false };
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authService = new AuthService();
|
export const authService = new AuthService();
|
||||||
|
|
|
||||||
234
bun.lock
234
bun.lock
|
|
@ -5,6 +5,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "zerobyte",
|
"name": "zerobyte",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-auth/sso": "^1.4.18",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
|
@ -114,6 +116,8 @@
|
||||||
|
|
||||||
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
||||||
|
|
||||||
|
"@authenio/xml-encryption": ["@authenio/xml-encryption@2.0.2", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "escape-html": "^1.0.3", "xpath": "0.0.32" } }, "sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg=="],
|
||||||
|
|
||||||
"@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="],
|
"@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="],
|
||||||
|
|
||||||
"@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
|
"@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
|
||||||
|
|
@ -122,7 +126,7 @@
|
||||||
|
|
||||||
"@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="],
|
"@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="],
|
||||||
|
|
||||||
"@azure/core-http-compat": ["@azure/core-http-compat@2.3.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g=="],
|
"@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="],
|
||||||
|
|
||||||
"@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="],
|
"@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="],
|
||||||
|
|
||||||
|
|
@ -142,11 +146,11 @@
|
||||||
|
|
||||||
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
|
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
|
||||||
|
|
||||||
"@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="],
|
"@azure/msal-browser": ["@azure/msal-browser@4.29.0", "", { "dependencies": { "@azure/msal-common": "15.15.0" } }, "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w=="],
|
||||||
|
|
||||||
"@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="],
|
"@azure/msal-common": ["@azure/msal-common@15.15.0", "", {}, "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw=="],
|
||||||
|
|
||||||
"@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="],
|
"@azure/msal-node": ["@azure/msal-node@3.8.8", "", { "dependencies": { "@azure/msal-common": "15.15.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
|
|
@ -212,6 +216,8 @@
|
||||||
|
|
||||||
"@better-auth/core": ["@better-auth/core@1.4.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA=="],
|
"@better-auth/core": ["@better-auth/core@1.4.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA=="],
|
||||||
|
|
||||||
|
"@better-auth/sso": ["@better-auth/sso@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.3.5" }, "peerDependencies": { "better-auth": "1.4.19", "better-call": "1.1.8" } }, "sha512-4OVPii6DQgIRm/DqO74Q4tZHx0LlNHXif3qrW7/iDzRO507h/AVAoJ9/U2fGycsPxUtPh1BT3PGHfX09sbF9Bw=="],
|
||||||
|
|
||||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.19" } }, "sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw=="],
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.19" } }, "sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw=="],
|
||||||
|
|
||||||
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
@ -234,57 +240,57 @@
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||||
|
|
||||||
"@faker-js/faker": ["@faker-js/faker@10.3.0", "", {}, "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw=="],
|
"@faker-js/faker": ["@faker-js/faker@10.3.0", "", {}, "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw=="],
|
||||||
|
|
||||||
|
|
@ -596,6 +602,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
|
||||||
|
|
||||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||||
|
|
||||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
@ -606,6 +614,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||||
|
|
||||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||||
|
|
||||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||||
|
|
@ -656,55 +666,55 @@
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||||
|
|
||||||
"@scalar/core": ["@scalar/core@0.3.42", "", { "dependencies": { "@scalar/types": "0.6.7" } }, "sha512-RbyooMuG4oQEOhiA/tC+++bkIK1zeYGNxrTzSAgTrTzVlbFKPzw72fs4gX9/eHDo7qVc9FsymIW6qVpWbySzNg=="],
|
"@scalar/core": ["@scalar/core@0.3.42", "", { "dependencies": { "@scalar/types": "0.6.7" } }, "sha512-RbyooMuG4oQEOhiA/tC+++bkIK1zeYGNxrTzSAgTrTzVlbFKPzw72fs4gX9/eHDo7qVc9FsymIW6qVpWbySzNg=="],
|
||||||
|
|
||||||
|
|
@ -876,15 +886,19 @@
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="],
|
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
|
||||||
|
|
||||||
|
"@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="],
|
||||||
|
|
||||||
|
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||||
|
|
||||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||||
|
|
||||||
|
|
@ -908,6 +922,8 @@
|
||||||
|
|
||||||
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
"arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="],
|
||||||
|
|
||||||
|
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||||
|
|
||||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||||
|
|
||||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||||
|
|
@ -918,7 +934,7 @@
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||||
|
|
||||||
"better-auth": ["better-auth@1.4.19", "", { "dependencies": { "@better-auth/core": "1.4.19", "@better-auth/telemetry": "1.4.19", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ=="],
|
"better-auth": ["better-auth@1.4.19", "", { "dependencies": { "@better-auth/core": "1.4.19", "@better-auth/telemetry": "1.4.19", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ=="],
|
||||||
|
|
||||||
|
|
@ -944,7 +960,9 @@
|
||||||
|
|
||||||
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="],
|
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
|
||||||
|
|
||||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
|
|
@ -978,7 +996,7 @@
|
||||||
|
|
||||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||||
|
|
||||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
|
|
@ -1076,13 +1094,13 @@
|
||||||
|
|
||||||
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
|
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.15-859cf75", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-Y36s1XQGVb1PgU3aRNgufp1K3D2VkIifu8kv4Ubsmxi+Dq+N7KMklnpp7Knu/XC4FZi2MHPPG3v3o097r0/TcQ=="],
|
"drizzle-kit": ["drizzle-kit@1.0.0-beta.9-e89174b", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-Xrw3k8E2CbSZr+kqe3k5W4oxd2fbEyczjKtyGIkAq0x9Wqpa/VtAT6Mkh83sIzqG4OSN7lOoUafsDxSE/AR7RA=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@1.0.0-beta.9-e89174b", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-B5KR/qYMZ0JMOurK+0xi1ObpOQcgrjaC9wHUiU2eTJjLemuh2CoQIw6yur68NxZG6Xcd0b9qghUNC/78/bEfbg=="],
|
"drizzle-orm": ["drizzle-orm@1.0.0-beta.9-e89174b", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-B5KR/qYMZ0JMOurK+0xi1ObpOQcgrjaC9wHUiU2eTJjLemuh2CoQIw6yur68NxZG6Xcd0b9qghUNC/78/bEfbg=="],
|
||||||
|
|
||||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||||
|
|
||||||
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
||||||
|
|
||||||
|
|
@ -1092,10 +1110,12 @@
|
||||||
|
|
||||||
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
|
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
|
|
@ -1118,6 +1138,8 @@
|
||||||
|
|
||||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
|
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
|
||||||
|
|
||||||
|
"fast-xml-parser": ["fast-xml-parser@5.3.7", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||||
|
|
@ -1132,7 +1154,7 @@
|
||||||
|
|
||||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.13.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w=="],
|
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||||
|
|
||||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
|
@ -1142,7 +1164,7 @@
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"h3": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="],
|
"h3": ["h3@2.0.1-rc.14", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.11.2" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-163qbGmTr/9rqQRNuqMqtgXnOUAkE4KTdauiC9y0E5iG1I65kte9NyfWvZw5RTDMt6eY+DtyoNzrQ9wA2BfvGQ=="],
|
||||||
|
|
||||||
"h3-v2": ["h3@2.0.1-rc.14", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.11.2" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-163qbGmTr/9rqQRNuqMqtgXnOUAkE4KTdauiC9y0E5iG1I65kte9NyfWvZw5RTDMt6eY+DtyoNzrQ9wA2BfvGQ=="],
|
"h3-v2": ["h3@2.0.1-rc.14", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.11.2" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-163qbGmTr/9rqQRNuqMqtgXnOUAkE4KTdauiC9y0E5iG1I65kte9NyfWvZw5RTDMt6eY+DtyoNzrQ9wA2BfvGQ=="],
|
||||||
|
|
||||||
|
|
@ -1206,7 +1228,7 @@
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||||
|
|
||||||
"isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
|
"isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
|
||||||
|
|
||||||
|
|
@ -1316,7 +1338,7 @@
|
||||||
|
|
||||||
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||||
|
|
||||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||||
|
|
||||||
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||||
|
|
||||||
|
|
@ -1428,13 +1450,17 @@
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
|
"node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
|
|
||||||
|
"node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="],
|
||||||
|
|
||||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
"nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="],
|
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
|
||||||
|
|
||||||
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
|
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
|
||||||
|
|
||||||
|
|
@ -1454,6 +1480,8 @@
|
||||||
|
|
||||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.15.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.15.0", "@oxlint-tsgolint/darwin-x64": "0.15.0", "@oxlint-tsgolint/linux-arm64": "0.15.0", "@oxlint-tsgolint/linux-x64": "0.15.0", "@oxlint-tsgolint/win32-arm64": "0.15.0", "@oxlint-tsgolint/win32-x64": "0.15.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-iwvFmhKQVZzVTFygUVI4t2S/VKEm+Mqkw3jQRJwfDuTcUYI5LCIYzdO5Dbuv4mFOkXZCcXaRRh0m+uydB5xdqw=="],
|
"oxlint-tsgolint": ["oxlint-tsgolint@0.15.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.15.0", "@oxlint-tsgolint/darwin-x64": "0.15.0", "@oxlint-tsgolint/linux-arm64": "0.15.0", "@oxlint-tsgolint/linux-x64": "0.15.0", "@oxlint-tsgolint/win32-arm64": "0.15.0", "@oxlint-tsgolint/win32-x64": "0.15.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-iwvFmhKQVZzVTFygUVI4t2S/VKEm+Mqkw3jQRJwfDuTcUYI5LCIYzdO5Dbuv4mFOkXZCcXaRRh0m+uydB5xdqw=="],
|
||||||
|
|
||||||
|
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||||
|
|
||||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||||
|
|
||||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
@ -1546,7 +1574,7 @@
|
||||||
|
|
||||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||||
|
|
||||||
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
|
|
@ -1556,6 +1584,8 @@
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"samlify": ["samlify@2.10.2", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.6", "camelcase": "^6.2.0", "node-forge": "^1.3.0", "node-rsa": "^1.1.1", "pako": "^1.0.10", "uuid": "^8.3.2", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.32" } }, "sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
@ -1590,6 +1620,8 @@
|
||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
|
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
||||||
|
|
||||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
|
|
@ -1604,7 +1636,7 @@
|
||||||
|
|
||||||
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
||||||
|
|
||||||
"tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
"tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
|
|
@ -1634,13 +1666,13 @@
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@5.4.3", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA=="],
|
"type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||||
|
|
||||||
"undici": ["undici@7.20.0", "", {}, "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ=="],
|
"undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
|
|
@ -1686,7 +1718,7 @@
|
||||||
|
|
||||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
|
||||||
|
|
||||||
"wait-for-expect": ["wait-for-expect@4.0.0", "", {}, "sha512-mcH2HYUUHhdFGHVJkgwkBxRihZO4VSuPyh6xhYHz7LEnYkcaLbTAEEsTpYiFw4UY45XdTZYYIaquuMucw9wWMw=="],
|
"wait-for-expect": ["wait-for-expect@4.0.0", "", {}, "sha512-mcH2HYUUHhdFGHVJkgwkBxRihZO4VSuPyh6xhYHz7LEnYkcaLbTAEEsTpYiFw4UY45XdTZYYIaquuMucw9wWMw=="],
|
||||||
|
|
||||||
|
|
@ -1708,8 +1740,16 @@
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||||
|
|
||||||
|
"xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="],
|
||||||
|
|
||||||
|
"xml-crypto": ["xml-crypto@6.1.2", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w=="],
|
||||||
|
|
||||||
|
"xml-escape": ["xml-escape@1.1.0", "", {}, "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg=="],
|
||||||
|
|
||||||
"xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="],
|
"xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="],
|
||||||
|
|
||||||
|
"xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||||
|
|
@ -1726,8 +1766,6 @@
|
||||||
|
|
||||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@happy-dom/global-registrator/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
||||||
|
|
||||||
"@hey-api/shared/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"@hey-api/shared/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
@ -1738,6 +1776,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
"@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
||||||
|
|
@ -1750,7 +1790,7 @@
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit/immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="],
|
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
||||||
|
|
||||||
"@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
"@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||||
|
|
||||||
|
|
@ -1780,41 +1820,29 @@
|
||||||
|
|
||||||
"@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@types/mssql/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
|
||||||
|
|
||||||
"@types/readable-stream/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
|
||||||
|
|
||||||
"@types/ws/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
|
||||||
|
|
||||||
"c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
|
||||||
|
|
||||||
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||||
|
|
||||||
"cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
"cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
"dotenv-cli/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
|
||||||
|
|
||||||
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
|
|
||||||
"encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
"h3-v2/srvx": ["srvx@0.11.4", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-m/2p87bqWZ94xpRN06qNBwh0xq/D0dXajnvPDSHFqrTogxuTWYNP1UHz6Cf+oY7D+NPLY35TJAp4ESIKn0WArQ=="],
|
"h3/srvx": ["srvx@0.11.7", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-p9qj9wkv/MqG1VoJpOsqXv1QcaVcYRk7ifsC6i3TEwDXFyugdhJN4J3KzQPZq2IJJ2ZCt7ASOB++85pEK38jRw=="],
|
||||||
|
|
||||||
"happy-dom/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
"h3-v2/srvx": ["srvx@0.11.7", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-p9qj9wkv/MqG1VoJpOsqXv1QcaVcYRk7ifsC6i3TEwDXFyugdhJN4J3KzQPZq2IJJ2ZCt7ASOB++85pEK38jRw=="],
|
||||||
|
|
||||||
"jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
|
||||||
|
|
||||||
"libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
"libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||||
|
|
||||||
"mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
"mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
"mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||||
|
|
||||||
|
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
|
|
@ -1824,29 +1852,15 @@
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"tedious/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
|
||||||
|
|
||||||
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
||||||
|
|
||||||
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="],
|
||||||
|
|
||||||
"@azure/identity/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
"@azure/identity/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
|
|
||||||
"@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
"@types/mssql/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
"@types/readable-stream/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
|
|
||||||
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
|
|
||||||
"happy-dom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
|
|
||||||
"tedious/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
|
|
||||||
"@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,25 @@ services:
|
||||||
- DISABLE_RATE_LIMITING=true
|
- DISABLE_RATE_LIMITING=true
|
||||||
- APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b
|
- APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b
|
||||||
- BASE_URL=http://localhost:4096
|
- BASE_URL=http://localhost:4096
|
||||||
|
- TRUSTED_ORIGINS=http://dex:5557,http://localhost:5557
|
||||||
devices:
|
devices:
|
||||||
- /dev/fuse:/dev/fuse
|
- /dev/fuse:/dev/fuse
|
||||||
cap_add:
|
cap_add:
|
||||||
- SYS_ADMIN
|
- SYS_ADMIN
|
||||||
ports:
|
ports:
|
||||||
- "4096:4096"
|
- "4096:4096"
|
||||||
|
depends_on:
|
||||||
|
- dex
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- ./playwright/data:/var/lib/zerobyte/data
|
- ./playwright/data:/var/lib/zerobyte/data
|
||||||
- ./playwright/temp:/test-data
|
- ./playwright/temp:/test-data
|
||||||
|
|
||||||
|
dex:
|
||||||
|
image: ghcr.io/dexidp/dex:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5557:5557"
|
||||||
|
volumes:
|
||||||
|
- ./playwright/dex-config.yaml:/etc/dex/cfg.yaml:ro
|
||||||
|
command: dex serve /etc/dex/cfg.yaml
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,115 @@
|
||||||
import { expect, test } from "@playwright/test";
|
|
||||||
import path from "node:path";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import path from "node:path";
|
||||||
|
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||||
import { gotoAndWaitForAppReady } from "./helpers/page";
|
import { gotoAndWaitForAppReady } from "./helpers/page";
|
||||||
|
|
||||||
test.describe.configure({ mode: "serial" });
|
const testDataPath = path.join(process.cwd(), "playwright", "temp");
|
||||||
|
|
||||||
test.beforeAll(() => {
|
type ScenarioNames = {
|
||||||
const testDataPath = path.join(process.cwd(), "playwright", "temp");
|
volumeName: string;
|
||||||
if (fs.existsSync(testDataPath)) {
|
repositoryName: string;
|
||||||
for (const file of fs.readdirSync(testDataPath)) {
|
backupName: string;
|
||||||
fs.rmSync(path.join(testDataPath, file), { recursive: true, force: true });
|
};
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fs.mkdirSync(testDataPath, { recursive: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can backup & restore a file", async ({ page }) => {
|
function getRunId(testInfo: TestInfo) {
|
||||||
await gotoAndWaitForAppReady(page, "/");
|
return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`;
|
||||||
await expect(page).toHaveURL("/volumes");
|
}
|
||||||
|
|
||||||
// 0. Create a test file in /test-data
|
function getScenarioNames(runId: string): ScenarioNames {
|
||||||
const testDataPath = path.join(process.cwd(), "playwright", "temp");
|
return {
|
||||||
const filePath = path.join(testDataPath, "test.json");
|
volumeName: `Volume-${runId}`,
|
||||||
|
repositoryName: `Repo-${runId}`,
|
||||||
|
backupName: `Backup-${runId}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareTestFile(runId: string): string {
|
||||||
|
const runPath = path.join(testDataPath, runId);
|
||||||
|
fs.mkdirSync(runPath, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = path.join(runPath, "test.json");
|
||||||
fs.writeFileSync(filePath, JSON.stringify({ data: "test file" }));
|
fs.writeFileSync(filePath, JSON.stringify({ data: "test file" }));
|
||||||
|
|
||||||
// 1. Create a local volume on /test-data
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBackupScenario(page: Page, names: ScenarioNames) {
|
||||||
await page.getByRole("button", { name: "Create Volume" }).click();
|
await page.getByRole("button", { name: "Create Volume" }).click();
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test Volume");
|
await page.getByRole("textbox", { name: "Name" }).fill(names.volumeName);
|
||||||
await page.getByRole("button", { name: "test-data" }).click();
|
await page.getByRole("button", { name: "test-data" }).click();
|
||||||
await page.getByRole("button", { name: "Create Volume" }).click();
|
await page.getByRole("button", { name: "Create Volume" }).click();
|
||||||
await expect(page.getByText("Volume created successfully")).toBeVisible();
|
await expect(page.getByText("Volume created successfully")).toBeVisible();
|
||||||
|
|
||||||
// 2. Create a local repository on the default location
|
|
||||||
await page.getByRole("link", { name: "Repositories" }).click();
|
await page.getByRole("link", { name: "Repositories" }).click();
|
||||||
await page.getByRole("button", { name: "Create repository" }).click();
|
await page.getByRole("button", { name: "Create repository" }).click();
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test Repo");
|
await page.getByRole("textbox", { name: "Name" }).fill(names.repositoryName);
|
||||||
await page.getByRole("combobox", { name: "Backend" }).click();
|
await page.getByRole("combobox", { name: "Backend" }).click();
|
||||||
await page.getByRole("option", { name: "Local" }).click();
|
await page.getByRole("option", { name: "Local" }).click();
|
||||||
await page.getByRole("button", { name: "Create repository" }).click();
|
await page.getByRole("button", { name: "Create repository" }).click();
|
||||||
await expect(page.getByText("Repository created successfully")).toBeVisible();
|
await expect(page.getByText("Repository created successfully")).toBeVisible({ timeout: 30000 });
|
||||||
|
|
||||||
// 3. Create a backup schedule
|
|
||||||
await page.getByRole("link", { name: "Backups" }).click();
|
await page.getByRole("link", { name: "Backups" }).click();
|
||||||
await page.getByRole("button", { name: "Create a backup job" }).click();
|
const createBackupButton = page.getByRole("button", { name: "Create a backup job" }).first();
|
||||||
|
if (await createBackupButton.isVisible()) {
|
||||||
|
await createBackupButton.click();
|
||||||
|
} else {
|
||||||
|
await page.getByRole("link", { name: "Create a backup job" }).first().click();
|
||||||
|
}
|
||||||
await page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" }).click();
|
await page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" }).click();
|
||||||
await page.getByRole("option", { name: "Test Volume" }).click();
|
await page.getByRole("option", { name: names.volumeName }).click();
|
||||||
await page.getByRole("textbox", { name: "Backup name" }).fill("Test Backup");
|
await page.getByRole("textbox", { name: "Backup name" }).fill(names.backupName);
|
||||||
await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click();
|
await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click();
|
||||||
await page.getByRole("option", { name: "Test Repo" }).click();
|
await page.getByRole("option", { name: names.repositoryName }).click();
|
||||||
await page.getByRole("combobox").filter({ hasText: "Select frequency" }).click();
|
await page.getByRole("combobox").filter({ hasText: "Select frequency" }).click();
|
||||||
await page.getByRole("option", { name: "Daily" }).click();
|
await page.getByRole("option", { name: "Daily" }).click();
|
||||||
await page.getByRole("textbox", { name: "Execution time" }).fill("00:00");
|
await page.getByRole("textbox", { name: "Execution time" }).fill("00:00");
|
||||||
await page.getByRole("button", { name: "Create" }).click();
|
await page.getByRole("button", { name: "Create" }).click();
|
||||||
await expect(page.getByText("Backup job created successfully")).toBeVisible();
|
await expect(page.getByText("Backup job created successfully")).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
test("can backup & restore a file", async ({ page }, testInfo) => {
|
||||||
|
const runId = getRunId(testInfo);
|
||||||
|
const names = getScenarioNames(runId);
|
||||||
|
const filePath = prepareTestFile(runId);
|
||||||
|
|
||||||
|
await gotoAndWaitForAppReady(page, "/");
|
||||||
|
await expect(page).toHaveURL("/volumes");
|
||||||
|
|
||||||
|
await createBackupScenario(page, names);
|
||||||
|
|
||||||
// 4. Runs that schedule once
|
|
||||||
await page.getByRole("button", { name: "Backup now" }).click();
|
await page.getByRole("button", { name: "Backup now" }).click();
|
||||||
await expect(page.getByText("Backup started successfully")).toBeVisible();
|
await expect(page.getByText("Backup started successfully")).toBeVisible();
|
||||||
await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 });
|
await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 });
|
||||||
|
|
||||||
// 5. Modify the json file after the backup
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify({ data: "modified file" }));
|
fs.writeFileSync(filePath, JSON.stringify({ data: "modified file" }));
|
||||||
|
|
||||||
// 6. Restores the file from backup
|
await page
|
||||||
await page.getByRole("button", { name: /20 B/ }).click();
|
.getByRole("button", { name: /\d+ B$/ })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
await page.getByRole("link", { name: "Restore" }).click();
|
await page.getByRole("link", { name: "Restore" }).click();
|
||||||
await expect(page).toHaveURL(/\/restore/);
|
await expect(page).toHaveURL(/\/restore/);
|
||||||
await page.getByRole("button", { name: "Restore All" }).click();
|
await page.getByRole("button", { name: "Restore All" }).click();
|
||||||
await expect(page.getByText("Restore completed")).toBeVisible({ timeout: 30000 });
|
await expect(page.getByText("Restore completed")).toBeVisible({ timeout: 30000 });
|
||||||
|
|
||||||
// 7. Ensures that the file is back to its previous state
|
|
||||||
const restoredContent = fs.readFileSync(filePath, "utf8");
|
const restoredContent = fs.readFileSync(filePath, "utf8");
|
||||||
expect(JSON.parse(restoredContent)).toEqual({ data: "test file" });
|
expect(JSON.parse(restoredContent)).toEqual({ data: "test file" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("deleting a volume cascades and removes its backup schedule", async ({ page }) => {
|
test("deleting a volume cascades and removes its backup schedule", async ({ page }, testInfo) => {
|
||||||
await gotoAndWaitForAppReady(page, "/backups");
|
const runId = getRunId(testInfo);
|
||||||
await page.getByText("Test Backup", { exact: true }).first().click();
|
const names = getScenarioNames(runId);
|
||||||
|
|
||||||
const volumeLink = page.locator("main").getByRole("link", { name: "Test Volume", exact: true }).first();
|
await gotoAndWaitForAppReady(page, "/");
|
||||||
|
await expect(page).toHaveURL("/volumes");
|
||||||
|
|
||||||
|
await createBackupScenario(page, names);
|
||||||
|
|
||||||
|
await gotoAndWaitForAppReady(page, "/backups");
|
||||||
|
await page.getByText(names.backupName, { exact: true }).first().click();
|
||||||
|
|
||||||
|
const volumeLink = page.locator("main").getByRole("link", { name: names.volumeName, exact: true }).first();
|
||||||
await expect(volumeLink).toBeVisible();
|
await expect(volumeLink).toBeVisible();
|
||||||
await volumeLink.click();
|
await volumeLink.click();
|
||||||
await expect(page).toHaveURL(/\/volumes\/[^/?#]+/);
|
await expect(page).toHaveURL(/\/volumes\/[^/?#]+/);
|
||||||
|
|
@ -95,5 +125,5 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page
|
||||||
await expect(page.getByText("Volume deleted successfully")).toBeVisible();
|
await expect(page.getByText("Volume deleted successfully")).toBeVisible();
|
||||||
|
|
||||||
await gotoAndWaitForAppReady(page, "/backups");
|
await gotoAndWaitForAppReady(page, "/backups");
|
||||||
await expect(page.getByText("Test Backup", { exact: true })).toHaveCount(0);
|
await expect(page.getByText(names.backupName, { exact: true })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
167
e2e/0003-oidc.spec.ts
Normal file
167
e2e/0003-oidc.spec.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
import { expect, test, type Browser, type Page } from "@playwright/test";
|
||||||
|
import { gotoAndWaitForAppReady, waitForAppReady } from "./helpers/page";
|
||||||
|
|
||||||
|
const dexOrigin = process.env.E2E_DEX_ORIGIN ?? "http://dex:5557";
|
||||||
|
const issuer = `${dexOrigin}/dex`;
|
||||||
|
const discoveryEndpoint = `${issuer}/.well-known/openid-configuration`;
|
||||||
|
const appBaseUrl = `http://${process.env.SERVER_IP ?? "localhost"}:4096`;
|
||||||
|
|
||||||
|
const providerIds = {
|
||||||
|
register: "test-oidc-register",
|
||||||
|
uninvited: "test-oidc-uninvited",
|
||||||
|
invited: "test-oidc-invited",
|
||||||
|
autoLink: "test-oidc",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const dexPassword = "password";
|
||||||
|
const uninvitedUserEmail = "admin@example.com";
|
||||||
|
const invitedUserEmail = "user@example.com";
|
||||||
|
const autoLinkTargetEmail = "test@example.com";
|
||||||
|
const autoLinkTargetUsername = "sso-link-target";
|
||||||
|
const inviteOnlyMessage =
|
||||||
|
"Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO.";
|
||||||
|
|
||||||
|
async function openSsoSettings(page: Page) {
|
||||||
|
await gotoAndWaitForAppReady(page, "/settings?tab=users");
|
||||||
|
await expect(page.getByText("Single Sign-On")).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerOidcProvider(page: Page, providerId: string) {
|
||||||
|
await gotoAndWaitForAppReady(page, "/settings/sso/new");
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Provider ID" }).fill(providerId);
|
||||||
|
await page.getByRole("textbox", { name: "Organization Domain" }).fill("example.com");
|
||||||
|
await page.getByRole("textbox", { name: "Issuer URL" }).fill(issuer);
|
||||||
|
await page.getByRole("textbox", { name: "Discovery Endpoint" }).fill(discoveryEndpoint);
|
||||||
|
await page.getByRole("textbox", { name: "Client ID" }).fill("zerobyte-test");
|
||||||
|
await page.getByRole("textbox", { name: "Client Secret" }).fill("test-secret-12345");
|
||||||
|
await page.getByRole("button", { name: "Register Provider" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText("SSO provider registered successfully")).toBeVisible();
|
||||||
|
await expect(page.getByRole("cell", { name: providerId, exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPendingInvitation(page: Page, email: string) {
|
||||||
|
const response = await page.request.post("/api/auth/organization/invite-member", {
|
||||||
|
headers: {
|
||||||
|
Origin: appBaseUrl,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
role: "member",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok()) {
|
||||||
|
throw new Error(`Failed to invite ${email}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAutoLinkTargetUser(page: Page, email: string, username: string) {
|
||||||
|
const response = await page.request.post("/api/auth/admin/create-user", {
|
||||||
|
headers: {
|
||||||
|
Origin: appBaseUrl,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
password: dexPassword,
|
||||||
|
name: "SSO Link Target",
|
||||||
|
role: "user",
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
hasDownloadedResticPassword: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok()) {
|
||||||
|
throw new Error(`Failed to create auto-link target user: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setProviderAutoLinking(page: Page, providerId: string, enabled: boolean) {
|
||||||
|
await openSsoSettings(page);
|
||||||
|
const providerRow = page
|
||||||
|
.getByRole("row")
|
||||||
|
.filter({ has: page.getByRole("cell", { name: providerId, exact: true }) })
|
||||||
|
.first();
|
||||||
|
const autoLinkSwitch = providerRow.getByRole("switch");
|
||||||
|
const expectedState = enabled ? "true" : "false";
|
||||||
|
const currentState = await autoLinkSwitch.getAttribute("aria-checked");
|
||||||
|
const expectedToast = enabled ? "Automatic account linking enabled" : "Automatic account linking disabled";
|
||||||
|
|
||||||
|
if (currentState !== expectedState) {
|
||||||
|
await autoLinkSwitch.click();
|
||||||
|
await expect(page.getByText(expectedToast)).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(autoLinkSwitch).toHaveAttribute("aria-checked", expectedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withOidcLoginAttempt(
|
||||||
|
browser: Browser,
|
||||||
|
providerId: string,
|
||||||
|
dexLogin: string,
|
||||||
|
assertions: (page: Page) => Promise<void>,
|
||||||
|
) {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
storageState: {
|
||||||
|
cookies: [],
|
||||||
|
origins: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoAndWaitForAppReady(page, `${appBaseUrl}/login`);
|
||||||
|
await page.getByRole("button", { name: `Log in with ${providerId}`, exact: true }).click();
|
||||||
|
await page.waitForURL(/\/dex\/auth/, { timeout: 60000 });
|
||||||
|
|
||||||
|
await page.locator('input[name="login"]').fill(dexLogin);
|
||||||
|
await page.locator('input[name="password"]').fill(dexPassword);
|
||||||
|
await page.locator('button[type="submit"]').click();
|
||||||
|
|
||||||
|
await assertions(page);
|
||||||
|
} finally {
|
||||||
|
await context.close().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("admin can register an OIDC provider", async ({ page }) => {
|
||||||
|
await registerOidcProvider(page, providerIds.register);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uninvited OIDC users are blocked", async ({ page, browser }) => {
|
||||||
|
await registerOidcProvider(page, providerIds.uninvited);
|
||||||
|
|
||||||
|
await withOidcLoginAttempt(browser, providerIds.uninvited, uninvitedUserEmail, async (ssoPage) => {
|
||||||
|
await ssoPage.waitForURL(/\/login(\/error)?/, { timeout: 60000 });
|
||||||
|
await waitForAppReady(ssoPage);
|
||||||
|
await expect(ssoPage.getByText(inviteOnlyMessage)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invited OIDC users can sign in", async ({ page, browser }) => {
|
||||||
|
await registerOidcProvider(page, providerIds.invited);
|
||||||
|
await createPendingInvitation(page, invitedUserEmail);
|
||||||
|
|
||||||
|
await withOidcLoginAttempt(browser, providerIds.invited, invitedUserEmail, async (ssoPage) => {
|
||||||
|
await ssoPage.waitForURL(/\/volumes/, { timeout: 60000 });
|
||||||
|
await waitForAppReady(ssoPage);
|
||||||
|
await expect(ssoPage).toHaveURL(/\/volumes/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("auto-link setting can be enabled for an OIDC provider", async ({ page, browser }) => {
|
||||||
|
await registerOidcProvider(page, providerIds.autoLink);
|
||||||
|
await createAutoLinkTargetUser(page, autoLinkTargetEmail, autoLinkTargetUsername);
|
||||||
|
await createPendingInvitation(page, autoLinkTargetEmail);
|
||||||
|
|
||||||
|
await setProviderAutoLinking(page, providerIds.autoLink, true);
|
||||||
|
|
||||||
|
await withOidcLoginAttempt(browser, providerIds.autoLink, autoLinkTargetEmail, async (ssoPage) => {
|
||||||
|
await ssoPage.waitForURL(/\/volumes/, { timeout: 60000 });
|
||||||
|
await waitForAppReady(ssoPage);
|
||||||
|
await expect(ssoPage).toHaveURL(/\/volumes/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"tsc": "tsc --noEmit",
|
"tsc": "tsc --noEmit",
|
||||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||||
"start:e2e": "docker compose down && docker compose up --build zerobyte-e2e",
|
"start:e2e": "docker compose down && rm -f playwright/data/zerobyte.db playwright/data/cache.db playwright/data/restic.pass playwright/.auth/user.json playwright/restic.pass && docker compose up --build zerobyte-e2e",
|
||||||
"start:distroless": "docker compose down && docker compose up --build zerobyte-distroless",
|
"start:distroless": "docker compose down && docker compose up --build zerobyte-distroless",
|
||||||
"gen:api-client": "dotenv -e .env.local -- openapi-ts",
|
"gen:api-client": "dotenv -e .env.local -- openapi-ts",
|
||||||
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
|
"gen:migrations": "dotenv -e .env.local -- drizzle-kit generate",
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"test:codegen": "playwright codegen localhost:4096"
|
"test:codegen": "playwright codegen localhost:4096"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-auth/sso": "^1.4.18",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|
@ -36,6 +37,7 @@
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ export default defineConfig({
|
||||||
testDir: "./e2e",
|
testDir: "./e2e",
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
|
timeout: 90000,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: "html",
|
reporter: "html",
|
||||||
use: {
|
use: {
|
||||||
baseURL: `http://${process.env.SERVER_IP}:4096`,
|
baseURL: `http://${process.env.SERVER_IP}:4096`,
|
||||||
|
|
@ -17,12 +17,16 @@ export default defineConfig({
|
||||||
{
|
{
|
||||||
name: "setup",
|
name: "setup",
|
||||||
testMatch: /.*\.setup\.ts/,
|
testMatch: /.*\.setup\.ts/,
|
||||||
|
workers: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
use: {
|
use: {
|
||||||
...devices["Desktop Chrome"],
|
...devices["Desktop Chrome"],
|
||||||
storageState: "playwright/.auth/user.json",
|
storageState: "playwright/.auth/user.json",
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--host-rules=MAP dex 127.0.0.1"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dependencies: ["setup"],
|
dependencies: ["setup"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
50
playwright/dex-config.yaml
Normal file
50
playwright/dex-config.yaml
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
issuer: http://dex:5557/dex
|
||||||
|
|
||||||
|
storage:
|
||||||
|
type: sqlite3
|
||||||
|
config:
|
||||||
|
file: /tmp/dex.db
|
||||||
|
|
||||||
|
web:
|
||||||
|
http: 0.0.0.0:5557
|
||||||
|
|
||||||
|
enablePasswordDB: true
|
||||||
|
|
||||||
|
staticPasswords:
|
||||||
|
- email: "admin@example.com"
|
||||||
|
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||||
|
username: "admin"
|
||||||
|
- email: "user@example.com"
|
||||||
|
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||||
|
username: "user"
|
||||||
|
- email: "test@example.com"
|
||||||
|
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||||
|
username: "test"
|
||||||
|
- email: "test@test.com"
|
||||||
|
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||||
|
username: "test-local"
|
||||||
|
|
||||||
|
staticClients:
|
||||||
|
- id: zerobyte-test
|
||||||
|
name: Zerobyte Test
|
||||||
|
redirectURIs:
|
||||||
|
- "http://localhost:3000/api/auth/sso/callback/credential"
|
||||||
|
- "http://localhost:3000/api/auth/sso/callback/dex"
|
||||||
|
- "http://localhost:4096/api/auth/sso/callback/test-oidc"
|
||||||
|
- "http://localhost:4096/api/auth/sso/callback/test-oidc-register"
|
||||||
|
- "http://localhost:4096/api/auth/sso/callback/test-oidc-uninvited"
|
||||||
|
- "http://localhost:4096/api/auth/sso/callback/test-oidc-invited"
|
||||||
|
- "http://localhost:4096/api/auth/sso/callback/test-oidc-autolink"
|
||||||
|
secret: test-secret-12345
|
||||||
|
|
||||||
|
oauth2:
|
||||||
|
skipApprovalScreen: true
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: debug
|
||||||
|
format: json
|
||||||
|
|
||||||
|
# Issuer URL: http://dex:5557/dex
|
||||||
|
# Discovery URL: http://dex:5557/dex/.well-known/openid-configuration
|
||||||
|
# Client ID: zerobyte-test
|
||||||
|
# Client Secret: test-secret-12345
|
||||||
Loading…
Reference in a new issue