@saena-io/create 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +313 -0
- package/package.json +24 -0
- package/template/addons/content/src/content/Hero.tsx +13 -0
- package/template/addons/content/src/content/admin.ts +5 -0
- package/template/addons/content/src/content/schema.ts +13 -0
- package/template/addons/content/src/content/sections.ts +6 -0
- package/template/addons/content/src/routes/index.tsx +21 -0
- package/template/addons/content/src/server/content.ts +25 -0
- package/template/base/.env.example +15 -0
- package/template/base/README.md +58 -0
- package/template/base/gitignore +14 -0
- package/template/base/package.json +40 -0
- package/template/base/scripts/migrate.ts +43 -0
- package/template/base/scripts/seed-staff.ts +68 -0
- package/template/base/src/admin/data.ts +261 -0
- package/template/base/src/admin/registry.ts +6 -0
- package/template/base/src/router.tsx +19 -0
- package/template/base/src/routes/__root.tsx +34 -0
- package/template/base/src/routes/admin/$.tsx +17 -0
- package/template/base/src/routes/admin/index.tsx +8 -0
- package/template/base/src/routes/admin/route.tsx +29 -0
- package/template/base/src/routes/api/assets/$.ts +125 -0
- package/template/base/src/routes/api/assets/upload.ts +99 -0
- package/template/base/src/routes/api/auth/staff/$.ts +12 -0
- package/template/base/src/routes/api/plugin/$.ts +51 -0
- package/template/base/src/routes/index.tsx +19 -0
- package/template/base/src/server/admin-context.ts +39 -0
- package/template/base/src/server/auth.ts +32 -0
- package/template/base/src/server/branding.ts +19 -0
- package/template/base/src/server/business-profile.ts +19 -0
- package/template/base/src/server/core.ts +41 -0
- package/template/base/src/server/plugin-host.ts +21 -0
- package/template/base/src/server/require-ctx.ts +25 -0
- package/template/base/src/server/team.ts +58 -0
- package/template/base/src/server/translations.ts +74 -0
- package/template/base/tsconfig.json +22 -0
- package/template/base/vite.config.ts +25 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
// Your public homepage. SAENA renders the admin at /admin; everything outside /admin is yours to build.
|
|
4
|
+
export const Route = createFileRoute('/')({ component: Home });
|
|
5
|
+
|
|
6
|
+
function Home() {
|
|
7
|
+
return (
|
|
8
|
+
<main className="mx-auto flex min-h-screen max-w-2xl flex-col justify-center gap-4 p-8">
|
|
9
|
+
<h1 className="font-semibold text-3xl tracking-tight">{{PROJECT_NAME}}</h1>
|
|
10
|
+
<p className="text-muted-foreground">
|
|
11
|
+
Your SAENA app is running. Manage content and settings in the{' '}
|
|
12
|
+
<a className="underline underline-offset-4" href="/admin">
|
|
13
|
+
admin
|
|
14
|
+
</a>
|
|
15
|
+
.
|
|
16
|
+
</p>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { serverPlugins } from '@/plugins';
|
|
2
|
+
import { PLUGIN_CATALOG } from '@saena-io/catalog';
|
|
3
|
+
import { describePredefinedRoles } from '@saena-io/core';
|
|
4
|
+
import {
|
|
5
|
+
type AdminContextData,
|
|
6
|
+
CORE_PERMISSIONS,
|
|
7
|
+
type Permission,
|
|
8
|
+
type PluginCatalogView,
|
|
9
|
+
} from '@saena-io/plugin-sdk';
|
|
10
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
11
|
+
import { requireCtx } from './require-ctx';
|
|
12
|
+
|
|
13
|
+
// The admin shell's per-request context (ADR-0012 phase 3 + roadmap M5). Returns the signed-in actor's resolved
|
|
14
|
+
// permission ids — for ADVISORY client-side nav/section gating (hiding what they can't use; the server fns are
|
|
15
|
+
// still the real boundary, §10) — a read-only reference of the predefined roles, and the plugin catalog annotated
|
|
16
|
+
// with what THIS project has installed (for the Help screen). Any authenticated staff may read it: it's their own
|
|
17
|
+
// permission set + the public role/plugin catalogues, so it gates on a session (requireCtx) without a permission.
|
|
18
|
+
export const getAdminContextFn = createServerFn({ method: 'GET' }).handler(
|
|
19
|
+
async (): Promise<AdminContextData> => {
|
|
20
|
+
const ctx = await requireCtx();
|
|
21
|
+
const permissions = ctx.actor.kind === 'staff' ? [...ctx.actor.permissions] : [];
|
|
22
|
+
const catalogue: Permission[] = [
|
|
23
|
+
...CORE_PERMISSIONS,
|
|
24
|
+
...serverPlugins.flatMap((p) => p.permissions ?? []),
|
|
25
|
+
];
|
|
26
|
+
const installed = new Set(serverPlugins.map((p) => p.id));
|
|
27
|
+
const plugins: PluginCatalogView[] = PLUGIN_CATALOG.map((e) => ({
|
|
28
|
+
id: e.id,
|
|
29
|
+
name: e.name,
|
|
30
|
+
summary: e.summary,
|
|
31
|
+
category: e.category,
|
|
32
|
+
package: e.package,
|
|
33
|
+
dependsOn: e.dependsOn,
|
|
34
|
+
status: e.status,
|
|
35
|
+
installed: installed.has(e.id),
|
|
36
|
+
}));
|
|
37
|
+
return { permissions, roles: describePredefinedRoles(catalogue), plugins };
|
|
38
|
+
},
|
|
39
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createStaffAuth, resolveActor } from '@saena-io/core';
|
|
2
|
+
import type { Actor } from '@saena-io/plugin-sdk';
|
|
3
|
+
import { getCoreDb } from './core';
|
|
4
|
+
|
|
5
|
+
const SECRET = process.env.BETTER_AUTH_SECRET ?? 'dev-secret-change-me-0123456789abcdef';
|
|
6
|
+
// Must match the dev server origin (WEB_PORT) so better-auth's origin/CSRF check passes.
|
|
7
|
+
const BASE_URL = process.env.BETTER_AUTH_URL ?? 'http://localhost:3100';
|
|
8
|
+
|
|
9
|
+
// Server-only: the staff (admin) better-auth audience (§10). The customer audience moved to @saena-io/customers
|
|
10
|
+
// (roadmap M0); a logged-in storefront wires its instance (createCustomerAuth) post-launch. Lazy so a build
|
|
11
|
+
// doesn't require DATABASE_URL at import.
|
|
12
|
+
let staffAuth: ReturnType<typeof createStaffAuth> | null = null;
|
|
13
|
+
|
|
14
|
+
export function getStaffAuth() {
|
|
15
|
+
if (!staffAuth) {
|
|
16
|
+
staffAuth = createStaffAuth(getCoreDb(), {
|
|
17
|
+
secret: SECRET,
|
|
18
|
+
baseURL: BASE_URL,
|
|
19
|
+
basePath: '/api/auth/staff',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return staffAuth;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The auth-vendor seam (roadmap M0): resolve a request's `Actor` from the staff better-auth session. Passed to
|
|
27
|
+
* `createRequestContext` as `resolveActor`, so core's request context carries no auth-vendor dependency — this one
|
|
28
|
+
* function is the single point a different identity provider (Clerk/Auth0) would replace.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveRequestActor(headers: Headers): Promise<Actor> {
|
|
31
|
+
return resolveActor({ staff: getStaffAuth() }, getCoreDb(), headers);
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getBranding, upsertBranding } from '@saena-io/core';
|
|
2
|
+
import { brandingInputSchema } from '@saena-io/plugin-sdk';
|
|
3
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
4
|
+
import { getCoreDb } from './core';
|
|
5
|
+
import { requireCtx } from './require-ctx';
|
|
6
|
+
|
|
7
|
+
/** Read the editable branding — null before it's first configured. */
|
|
8
|
+
export const getBrandingFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
9
|
+
(await requireCtx()).requirePermission('settings.manage');
|
|
10
|
+
return getBranding(getCoreDb());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** Persist branding, validated against the shared schema. */
|
|
14
|
+
export const saveBrandingFn = createServerFn({ method: 'POST' })
|
|
15
|
+
.validator(brandingInputSchema)
|
|
16
|
+
.handler(async ({ data }) => {
|
|
17
|
+
(await requireCtx()).requirePermission('settings.manage');
|
|
18
|
+
return upsertBranding(getCoreDb(), data);
|
|
19
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getBusinessProfile, upsertBusinessProfile } from '@saena-io/core';
|
|
2
|
+
import { businessProfileInputSchema } from '@saena-io/plugin-sdk';
|
|
3
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
4
|
+
import { getCoreDb } from './core';
|
|
5
|
+
import { requireCtx } from './require-ctx';
|
|
6
|
+
|
|
7
|
+
/** Read the editable business profile — null before it's first configured. */
|
|
8
|
+
export const getBusinessProfileFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
9
|
+
(await requireCtx()).requirePermission('settings.manage');
|
|
10
|
+
return getBusinessProfile(getCoreDb());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** Persist the business profile, validated against the shared schema. */
|
|
14
|
+
export const saveBusinessProfileFn = createServerFn({ method: 'POST' })
|
|
15
|
+
.validator(businessProfileInputSchema)
|
|
16
|
+
.handler(async ({ data }) => {
|
|
17
|
+
(await requireCtx()).requirePermission('settings.manage');
|
|
18
|
+
return upsertBusinessProfile(getCoreDb(), data);
|
|
19
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Db,
|
|
3
|
+
createCoreContext,
|
|
4
|
+
createDb,
|
|
5
|
+
createLocalDiskProvider,
|
|
6
|
+
createStorageService,
|
|
7
|
+
} from '@saena-io/core';
|
|
8
|
+
import type { CoreContext } from '@saena-io/plugin-sdk';
|
|
9
|
+
|
|
10
|
+
// Server-only: the core's Postgres handle, opened lazily (a build — or a request that never touches
|
|
11
|
+
// core data — doesn't need DATABASE_URL at import time). Shared across the admin server functions.
|
|
12
|
+
let db: Db | null = null;
|
|
13
|
+
|
|
14
|
+
export function getCoreDb(): Db {
|
|
15
|
+
if (!db) {
|
|
16
|
+
const connectionString = process.env.DATABASE_URL;
|
|
17
|
+
if (!connectionString) {
|
|
18
|
+
throw new Error('DATABASE_URL is not set — admin data access needs Postgres (see .env).');
|
|
19
|
+
}
|
|
20
|
+
db = createDb(connectionString);
|
|
21
|
+
}
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The system CoreContext (db + i18n + profile + events + warn-only jobs), built once. The plugin host and
|
|
26
|
+
// the /api/plugin dispatch share it; the dispatch wraps it per-request into a RequestContext (actor + locale).
|
|
27
|
+
let coreContext: CoreContext | null = null;
|
|
28
|
+
|
|
29
|
+
export function getCoreContext(): CoreContext {
|
|
30
|
+
if (!coreContext) {
|
|
31
|
+
const db = getCoreDb();
|
|
32
|
+
// Asset storage (ADR-0002): the local/Railway-disk provider by default. This is the seam to swap in an
|
|
33
|
+
// S3/R2 adapter later — set SAENA_ASSETS_DIR to a mounted volume in production.
|
|
34
|
+
const storage = createStorageService({
|
|
35
|
+
db,
|
|
36
|
+
provider: createLocalDiskProvider({ dir: process.env.SAENA_ASSETS_DIR ?? '.data/assets' }),
|
|
37
|
+
});
|
|
38
|
+
coreContext = createCoreContext({ db, storage });
|
|
39
|
+
}
|
|
40
|
+
return coreContext;
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { serverPlugins } from '@/plugins';
|
|
2
|
+
import { type PluginHost, createPluginHost } from '@saena-io/core';
|
|
3
|
+
import { getCoreContext } from './core';
|
|
4
|
+
|
|
5
|
+
// Server-only: the plugin host as a once-per-process singleton (mirrors getCoreDb). Runtime only — it wires
|
|
6
|
+
// event subscriptions (host.start) and exposes the ordered plugins so the /api/plugin dispatch can resolve a
|
|
7
|
+
// plugin's server functions (ADR-0006). The MUTATING lifecycle (migrateCore → runPluginMigrations →
|
|
8
|
+
// host.install) runs at deploy time via scripts/migrate.ts, never here in a request path.
|
|
9
|
+
let host: PluginHost | null = null;
|
|
10
|
+
|
|
11
|
+
export function getPluginHost(): PluginHost {
|
|
12
|
+
if (!host) {
|
|
13
|
+
// Build the host on the SHARED system context (getCoreContext) — NOT a fresh one — so the capabilities the
|
|
14
|
+
// host registers from each plugin's `provides` (ADR-0010) live on the same registry the /api/plugin
|
|
15
|
+
// dispatch reads through its per-request RequestContext. A separate context would leave that registry empty
|
|
16
|
+
// and every `ctx.capabilities.require(...)` would throw.
|
|
17
|
+
host = createPluginHost(serverPlugins, getCoreContext());
|
|
18
|
+
host.start();
|
|
19
|
+
}
|
|
20
|
+
return host;
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequestContext } from '@saena-io/core';
|
|
2
|
+
import type { RequestContext } from '@saena-io/plugin-sdk';
|
|
3
|
+
import { getRequest } from '@tanstack/react-start/server';
|
|
4
|
+
import { resolveRequestActor } from './auth';
|
|
5
|
+
import { getCoreContext, getCoreDb } from './core';
|
|
6
|
+
|
|
7
|
+
// The authorization seam for admin server functions (ADR-0012). Builds a per-request RequestContext (resolved
|
|
8
|
+
// actor + locale) from the current request's headers — the same factory the /api/plugin dispatch + asset routes
|
|
9
|
+
// use. Each protected server fn does `const ctx = await requireCtx(); ctx.requirePermission('…')`; a missing
|
|
10
|
+
// permission throws PermissionDeniedError, which createServerFn surfaces to the client as an error (the admin's
|
|
11
|
+
// client-side gate is UX only — this is the real boundary, §10). Replaces the old session-only requireStaff.
|
|
12
|
+
export async function requireCtx(): Promise<RequestContext> {
|
|
13
|
+
return createRequestContext({
|
|
14
|
+
core: getCoreContext(),
|
|
15
|
+
db: getCoreDb(),
|
|
16
|
+
resolveActor: resolveRequestActor,
|
|
17
|
+
headers: getRequest().headers,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The signed-in staff member's id after a `staff.manage` check — for the team service's RBAC invariants. */
|
|
22
|
+
export function staffActorId(ctx: RequestContext): string {
|
|
23
|
+
if (ctx.actor.kind !== 'staff') throw new Error('staff actor required');
|
|
24
|
+
return ctx.actor.userId;
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStaffMember,
|
|
3
|
+
deleteStaffMember,
|
|
4
|
+
listRoles,
|
|
5
|
+
listStaff,
|
|
6
|
+
setStaffRole,
|
|
7
|
+
} from '@saena-io/core';
|
|
8
|
+
import { idRefSchema, staffCreateSchema, staffRoleUpdateSchema } from '@saena-io/plugin-sdk';
|
|
9
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
10
|
+
import { getStaffAuth } from './auth';
|
|
11
|
+
import { getCoreDb } from './core';
|
|
12
|
+
import { requireCtx, staffActorId } from './require-ctx';
|
|
13
|
+
|
|
14
|
+
// Team / RBAC server functions (§10). All gate on `staff.manage` (Owner+) via requirePermission — the real
|
|
15
|
+
// boundary (ADR-0012); the admin's client-side gate is UX only. Mutations thread the signed-in staff id so the
|
|
16
|
+
// team service can enforce its invariants (no privilege escalation, last-Admin protection, no self-removal). The
|
|
17
|
+
// predefined roles + the bootstrap Admin are seeded at deploy time by `ensureRoles` (migrate); roles are NOT
|
|
18
|
+
// editable here (no custom-role builder, ADR-0012 phase 3) — `listRolesFn` is read-only, for assignment.
|
|
19
|
+
|
|
20
|
+
/** List staff members. */
|
|
21
|
+
export const listStaffFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
22
|
+
(await requireCtx()).requirePermission('staff.manage');
|
|
23
|
+
return listStaff(getCoreDb());
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Create a staff member with an admin-set initial password. */
|
|
27
|
+
export const createStaffFn = createServerFn({ method: 'POST' })
|
|
28
|
+
.validator(staffCreateSchema)
|
|
29
|
+
.handler(async ({ data }) => {
|
|
30
|
+
const ctx = await requireCtx();
|
|
31
|
+
ctx.requirePermission('staff.manage');
|
|
32
|
+
return createStaffMember(getCoreDb(), getStaffAuth(), data, staffActorId(ctx));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Reassign (or clear) a staff member's role. */
|
|
36
|
+
export const updateStaffRoleFn = createServerFn({ method: 'POST' })
|
|
37
|
+
.validator(staffRoleUpdateSchema)
|
|
38
|
+
.handler(async ({ data }) => {
|
|
39
|
+
const ctx = await requireCtx();
|
|
40
|
+
ctx.requirePermission('staff.manage');
|
|
41
|
+
await setStaffRole(getCoreDb(), data.id, data.roleId, staffActorId(ctx));
|
|
42
|
+
return data;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Remove a staff member. */
|
|
46
|
+
export const deleteStaffFn = createServerFn({ method: 'POST' })
|
|
47
|
+
.validator(idRefSchema)
|
|
48
|
+
.handler(async ({ data }) => {
|
|
49
|
+
const ctx = await requireCtx();
|
|
50
|
+
ctx.requirePermission('staff.manage');
|
|
51
|
+
await deleteStaffMember(getCoreDb(), data.id, staffActorId(ctx));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** List the predefined roles (read-only) — the assignable set for the member role selector. */
|
|
55
|
+
export const listRolesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
56
|
+
(await requireCtx()).requirePermission('staff.manage');
|
|
57
|
+
return listRoles(getCoreDb());
|
|
58
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addLocale,
|
|
3
|
+
listLocales,
|
|
4
|
+
listTranslations,
|
|
5
|
+
removeLocale,
|
|
6
|
+
setMainLocale,
|
|
7
|
+
setReviewed,
|
|
8
|
+
setTranslation,
|
|
9
|
+
} from '@saena-io/core';
|
|
10
|
+
import {
|
|
11
|
+
localeInputSchema,
|
|
12
|
+
mainLocaleInputSchema,
|
|
13
|
+
translationReviewInputSchema,
|
|
14
|
+
translationValueInputSchema,
|
|
15
|
+
} from '@saena-io/plugin-sdk';
|
|
16
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
17
|
+
import { getCoreDb } from './core';
|
|
18
|
+
import { requireCtx } from './require-ctx';
|
|
19
|
+
|
|
20
|
+
// Host server functions for the i18n foundation (§9, ADR-0005). They reach core (where the db lives)
|
|
21
|
+
// and are injected into the shell as TanStack DB collections (see ../admin/data.ts), so @saena-io/admin
|
|
22
|
+
// never imports core. Auth is enforced server-side via requirePermission('translations.manage') (ADR-0012).
|
|
23
|
+
|
|
24
|
+
/** Every translatable value, grid-shaped (key × locale + status). Loaded whole; filtered client-side. */
|
|
25
|
+
export const listTranslationsFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
26
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
27
|
+
return listTranslations(getCoreDb());
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** The project's configured locales (exactly one is main). */
|
|
31
|
+
export const listLocalesFn = createServerFn({ method: 'GET' }).handler(async () => {
|
|
32
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
33
|
+
return listLocales(getCoreDb());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** Set a locale's value for a key — editing the main locale re-stales the other locales. */
|
|
37
|
+
export const setTranslationFn = createServerFn({ method: 'POST' })
|
|
38
|
+
.validator(translationValueInputSchema)
|
|
39
|
+
.handler(async ({ data }) => {
|
|
40
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
41
|
+
await setTranslation(getCoreDb(), data);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Lock/unlock a value's reviewed ("checked") state. */
|
|
45
|
+
export const setReviewedFn = createServerFn({ method: 'POST' })
|
|
46
|
+
.validator(translationReviewInputSchema)
|
|
47
|
+
.handler(async ({ data }) => {
|
|
48
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
49
|
+
await setReviewed(getCoreDb(), data);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Swap the main (source) locale — re-anchors staleness across every key. */
|
|
53
|
+
export const setMainLocaleFn = createServerFn({ method: 'POST' })
|
|
54
|
+
.validator(mainLocaleInputSchema)
|
|
55
|
+
.handler(async ({ data }) => {
|
|
56
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
57
|
+
await setMainLocale(getCoreDb(), data.code);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Add a project locale (a new target language). */
|
|
61
|
+
export const addLocaleFn = createServerFn({ method: 'POST' })
|
|
62
|
+
.validator(localeInputSchema)
|
|
63
|
+
.handler(async ({ data }) => {
|
|
64
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
65
|
+
await addLocale(getCoreDb(), data);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** Remove a locale and all its translations (refuses the main locale). */
|
|
69
|
+
export const removeLocaleFn = createServerFn({ method: 'POST' })
|
|
70
|
+
.validator(mainLocaleInputSchema)
|
|
71
|
+
.handler(async ({ data }) => {
|
|
72
|
+
(await requireCtx()).requirePermission('translations.manage');
|
|
73
|
+
await removeLocale(getCoreDb(), data.code);
|
|
74
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"types": ["vite/client", "node"],
|
|
16
|
+
"allowImportingTsExtensions": true,
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["src", "vite.config.ts"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
2
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
|
3
|
+
import viteReact from '@vitejs/plugin-react';
|
|
4
|
+
import { defineConfig, loadEnv } from 'vite';
|
|
5
|
+
|
|
6
|
+
export default defineConfig(({ mode }) => {
|
|
7
|
+
// Load this project's .env into process.env so the SSR server (the auth handler + db) sees
|
|
8
|
+
// DATABASE_URL / BETTER_AUTH_SECRET. Server-only — only VITE_-prefixed vars reach the client
|
|
9
|
+
// (via import.meta.env), and real env vars take precedence over the file.
|
|
10
|
+
for (const [key, value] of Object.entries(loadEnv(mode, process.cwd(), ''))) {
|
|
11
|
+
if (process.env[key] === undefined) process.env[key] = value;
|
|
12
|
+
}
|
|
13
|
+
const port = Number(process.env.PORT) || 3000;
|
|
14
|
+
return {
|
|
15
|
+
server: { port, host: true },
|
|
16
|
+
// Production server (`bun run start` → `vite preview`) binds the platform's PORT (e.g. Railway) on
|
|
17
|
+
// 0.0.0.0 so the deploy is reachable.
|
|
18
|
+
preview: { port, host: true },
|
|
19
|
+
resolve: { tsconfigPaths: true },
|
|
20
|
+
// The @saena-io/* packages ship TypeScript source (consumed via Vite). Bundle them for SSR so Vite
|
|
21
|
+
// transpiles them instead of treating them as pre-built external CommonJS.
|
|
22
|
+
ssr: { noExternal: [/@saena-io\//] },
|
|
23
|
+
plugins: [tailwindcss(), tanstackStart(), viteReact()],
|
|
24
|
+
};
|
|
25
|
+
});
|