feat: update client data
This commit is contained in:
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -8,6 +8,15 @@
|
|||||||
"source.organizeImports.biome": "explicit",
|
"source.organizeImports.biome": "explicit",
|
||||||
"source.fixAll.biome": "explicit"
|
"source.fixAll.biome": "explicit"
|
||||||
},
|
},
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/.git": true,
|
"**/.git": true,
|
||||||
"**/.svn": true,
|
"**/.svn": true,
|
||||||
@@ -15,5 +24,5 @@
|
|||||||
"**/CVS": true,
|
"**/CVS": true,
|
||||||
"**/.DS_Store": true,
|
"**/.DS_Store": true,
|
||||||
"**/Thumbs.db": true
|
"**/Thumbs.db": true
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@module/payment": "workspace:*",
|
"@module/payment": "workspace:*",
|
||||||
"@platform/config": "workspace:*",
|
"@platform/config": "workspace:*",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@tailwindcss/vite": "4.1.17",
|
"@tailwindcss/vite": "4.1.17",
|
||||||
"@tanstack/react-router": "1.139.9",
|
"@tanstack/react-router": "1.139.9",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"@valkyr/db": "npm:@jsr/valkyr__db@2.0.0",
|
"@valkyr/db": "npm:@jsr/valkyr__db@3.0.1",
|
||||||
"@zitadel/react": "1.1.0",
|
"@zitadel/react": "1.1.0",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
@@ -48,10 +48,12 @@
|
|||||||
"tailwind-merge": "3.4.0",
|
"tailwind-merge": "3.4.0",
|
||||||
"tailwindcss": "4.1.17",
|
"tailwindcss": "4.1.17",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.39.1",
|
||||||
|
"@tanstack/react-router-devtools": "1.144.0",
|
||||||
|
"@tanstack/router-plugin": "1.145.2",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "24.10.1",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { zitadel } from "../../services/zitadel.ts";
|
|
||||||
|
|
||||||
export function CallbackView() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
useEffect(() => {
|
|
||||||
async function handleCallback() {
|
|
||||||
try {
|
|
||||||
const user = await zitadel.userManager.signinRedirectCallback();
|
|
||||||
if (user) {
|
|
||||||
navigate({ to: "/", replace: true });
|
|
||||||
} else {
|
|
||||||
navigate({ to: "/", replace: true });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Callback error", error);
|
|
||||||
navigate({ to: "/", replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleCallback();
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { GalleryVerticalEnd } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Field, FieldDescription, FieldGroup, FieldSeparator } from "@/components/ui/field";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { zitadel } from "@/services/zitadel.ts";
|
|
||||||
|
|
||||||
export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
zitadel.authorize();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FieldGroup>
|
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
|
||||||
<a href="#" className="flex flex-col items-center gap-2 font-medium">
|
|
||||||
<div className="flex size-8 items-center justify-center rounded-md">
|
|
||||||
<GalleryVerticalEnd className="size-6" />
|
|
||||||
</div>
|
|
||||||
<span className="sr-only">Valkyr Sandbox</span>
|
|
||||||
</a>
|
|
||||||
<h1 className="text-xl font-bold">Welcome to Valkyr Sandbox</h1>
|
|
||||||
<FieldDescription>
|
|
||||||
Don't have an account? <a href="#">Sign up</a>
|
|
||||||
</FieldDescription>
|
|
||||||
</div>
|
|
||||||
<Field>
|
|
||||||
<Button type="submit">Login with Zitadel</Button>
|
|
||||||
</Field>
|
|
||||||
<FieldSeparator>Or</FieldSeparator>
|
|
||||||
<Field className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<Button variant="outline" type="button">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Continue with Apple
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" type="button">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
<FieldDescription className="px-6 text-center">
|
|
||||||
By clicking continue, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
|
|
||||||
</FieldDescription>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { LoginForm } from "./components/login-form.tsx";
|
|
||||||
|
|
||||||
export function LoginView() {
|
|
||||||
return (
|
|
||||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
|
||||||
<div className="w-full max-w-sm">
|
|
||||||
<LoginForm />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { Controller } from "@/lib/controller.tsx";
|
|
||||||
|
|
||||||
export class DashboardController extends Controller {}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { makeControllerComponent } from "@/lib/controller.tsx";
|
|
||||||
|
|
||||||
import { DashboardController } from "./dashboard.controller.ts";
|
|
||||||
|
|
||||||
export const DashboardView = makeControllerComponent(DashboardController, () => {
|
|
||||||
return <div>Dashboard</div>;
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GalleryVerticalEnd } from "lucide-react";
|
import { GalleryVerticalEnd } from "lucide-react";
|
||||||
|
|
||||||
import { Controller } from "@/lib/controller.tsx";
|
import { Controller } from "@/lib/controller.tsx";
|
||||||
import { User } from "@/services/user.ts";
|
import { auth } from "@/services/auth.ts";
|
||||||
|
|
||||||
type Tenant = {
|
type Tenant = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,7 +13,7 @@ export class AppSiderbarController extends Controller<{
|
|||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
}> {
|
}> {
|
||||||
async onInit() {
|
async onInit() {
|
||||||
const user = await User.resolve();
|
const user = auth.user;
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
return {
|
return {
|
||||||
tenant: {
|
tenant: {
|
||||||
|
|||||||
@@ -12,20 +12,25 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { LoadingSwap } from "@/components/ui/loading-swap";
|
import { LoadingSwap } from "@/components/ui/loading-swap";
|
||||||
import { SidebarMenuSubButton } from "@/components/ui/sidebar.tsx";
|
import { SidebarMenuSubButton } from "@/components/ui/sidebar.tsx";
|
||||||
|
|
||||||
import { makeControllerComponent } from "../../lib/controller.tsx";
|
import { makeControllerComponent } from "../../lib/controller.tsx";
|
||||||
import { CreateBeneficiaryController } from "./create-beneficiary.controller.ts";
|
import { FieldDescription, FieldGroup, FieldLabel } from "../ui/field.tsx";
|
||||||
|
import { CreateBeneficiaryController } from "./create.controller.ts";
|
||||||
|
|
||||||
export const DialogCreateBeneficiary = makeControllerComponent(
|
export const DialogCreateBeneficiary = makeControllerComponent(
|
||||||
CreateBeneficiaryController,
|
CreateBeneficiaryController,
|
||||||
({ title, setLabel, submit, isSubmitting, ...props }) => {
|
({ title, label, setLabel, submit, isSubmitting, ...props }) => {
|
||||||
const labelId = useId();
|
const labelId = useId();
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<form>
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<SidebarMenuSubButton className="cursor-pointer bg-secondary hover:bg-secondary/70">
|
<SidebarMenuSubButton className="cursor-pointer bg-secondary hover:bg-secondary/70">
|
||||||
<props.icon /> <span>{title}</span>
|
<props.icon /> <span>{title}</span>
|
||||||
@@ -33,20 +38,25 @@ export const DialogCreateBeneficiary = makeControllerComponent(
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create beneficiary</DialogTitle>
|
<DialogTitle>Create Beneficiary</DialogTitle>
|
||||||
<DialogDescription>Create a payment ledger and its supported currencies</DialogDescription>
|
<DialogDescription>Create a payment ledger and its supported currencies</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4">
|
<FieldGroup>
|
||||||
<div className="grid gap-3">
|
<FieldLabel htmlFor={labelId}>Label</FieldLabel>
|
||||||
<Label htmlFor={labelId}>Label</Label>
|
<Input
|
||||||
<Input id={labelId} name="label" onChange={({ target: { value } }) => setLabel(value)} />
|
id={labelId}
|
||||||
</div>
|
name="label"
|
||||||
</div>
|
value={label}
|
||||||
|
onChange={({ target: { value } }) => setLabel(value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FieldDescription>Enter the identifying label for the Beneficiary</FieldDescription>
|
||||||
|
</FieldGroup>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button variant="outline">Cancel</Button>
|
<Button variant="outline">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit" onClick={submit}>
|
<Button type="submit">
|
||||||
<LoadingSwap isLoading={isSubmitting}>Create</LoadingSwap>
|
<LoadingSwap isLoading={isSubmitting}>Create</LoadingSwap>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -3,32 +3,32 @@ import type { LucideIcon } from "lucide-react";
|
|||||||
import { payment } from "@/database/payment.ts";
|
import { payment } from "@/database/payment.ts";
|
||||||
import { Controller } from "@/lib/controller.tsx";
|
import { Controller } from "@/lib/controller.tsx";
|
||||||
import { api, getSuccessResponse } from "@/services/api.ts";
|
import { api, getSuccessResponse } from "@/services/api.ts";
|
||||||
import { User } from "@/services/user.ts";
|
import { auth } from "@/services/auth.ts";
|
||||||
|
|
||||||
export class CreateBeneficiaryController extends Controller<
|
export class CreateBeneficiaryController extends Controller<
|
||||||
{
|
{
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
|
label: string;
|
||||||
},
|
},
|
||||||
{ title: string; icon: LucideIcon }
|
{ title: string; icon: LucideIcon }
|
||||||
> {
|
> {
|
||||||
#label?: string;
|
|
||||||
|
|
||||||
async onInit() {
|
async onInit() {
|
||||||
return {
|
return {
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
label: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setLabel(label: string) {
|
setLabel(label: string) {
|
||||||
this.#label = label;
|
this.setState("label", label);
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.setState("isSubmitting", true);
|
this.setState("isSubmitting", true);
|
||||||
const res = await api.payment.benficiaries.create({
|
const res = await api.payment.benficiaries.create({
|
||||||
body: {
|
body: {
|
||||||
tenantId: await User.getTenantId(),
|
tenantId: auth.user.tenant.id,
|
||||||
label: this.#label,
|
label: this.state.label,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if ("data" in res) {
|
if ("data" in res) {
|
||||||
@@ -36,6 +36,9 @@ export class CreateBeneficiaryController extends Controller<
|
|||||||
.collection("beneficiary")
|
.collection("beneficiary")
|
||||||
.insertOne(await getSuccessResponse(api.payment.benficiaries.id({ params: { id: res.data } })));
|
.insertOne(await getSuccessResponse(api.payment.benficiaries.id({ params: { id: res.data } })));
|
||||||
}
|
}
|
||||||
this.setState("isSubmitting", false);
|
this.setState({
|
||||||
|
isSubmitting: false,
|
||||||
|
label: "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
app/src/components/beneficiary/read.component.tsx
Normal file
15
app/src/components/beneficiary/read.component.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { makeControllerComponent } from "@/lib/controller.tsx";
|
||||||
|
|
||||||
|
import { ReadBeneficiaryController } from "./read.controller.ts";
|
||||||
|
|
||||||
|
export const BeneficiaryComponent = makeControllerComponent(ReadBeneficiaryController, ({ beneficiary }) => {
|
||||||
|
if (beneficiary === undefined) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col pt-20 items-center justify-center text-center">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Beneficiary Not Found</h1>
|
||||||
|
<p className="text-muted-foreground mt-2 max-w-sm">Beneficiary with id "xxx" cannot be found.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <pre>{JSON.stringify(beneficiary, null, 2)}</pre>;
|
||||||
|
});
|
||||||
37
app/src/components/beneficiary/read.controller.ts
Normal file
37
app/src/components/beneficiary/read.controller.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Beneficiary } from "@module/payment/client";
|
||||||
|
|
||||||
|
import { payment } from "@/database/payment.ts";
|
||||||
|
import { Controller } from "@/lib/controller.tsx";
|
||||||
|
import { auth } from "@/services/auth.ts";
|
||||||
|
|
||||||
|
export class ReadBeneficiaryController extends Controller<
|
||||||
|
{
|
||||||
|
beneficiary?: Beneficiary;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
#subscription?: any;
|
||||||
|
|
||||||
|
async onInit() {
|
||||||
|
await this.#subscribe(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onResolve() {
|
||||||
|
await this.#subscribe(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDestroy(): Promise<void> {
|
||||||
|
this.#subscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #subscribe(_id: string) {
|
||||||
|
this.#subscription?.unsubscribe();
|
||||||
|
this.#subscription = await payment
|
||||||
|
.collection("beneficiary")
|
||||||
|
.subscribe({ _id, tenantId: auth.user.tenant.id }, { limit: 1 }, (beneficiary) => {
|
||||||
|
this.setState("beneficiary", beneficiary);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,10 @@ import type { Beneficiary } from "@module/payment/client";
|
|||||||
import { Book, CirclePlusIcon, type LucideIcon, SquareTerminal } from "lucide-react";
|
import { Book, CirclePlusIcon, type LucideIcon, SquareTerminal } from "lucide-react";
|
||||||
import type { FunctionComponent } from "react";
|
import type { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { DialogCreateBeneficiary } from "@/components/beneficiary/create.component.tsx";
|
||||||
import { loadBeneficiaries, payment } from "@/database/payment.ts";
|
import { loadBeneficiaries, payment } from "@/database/payment.ts";
|
||||||
import { Controller } from "@/lib/controller.tsx";
|
import { Controller } from "@/lib/controller.tsx";
|
||||||
import { User } from "@/services/user.ts";
|
import { auth } from "@/services/auth.ts";
|
||||||
|
|
||||||
import { DialogCreateBeneficiary } from "./payment/create-beneficiary.tsx";
|
|
||||||
|
|
||||||
export class NavPaymentController extends Controller<{
|
export class NavPaymentController extends Controller<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
@@ -19,7 +18,7 @@ export class NavPaymentController extends Controller<{
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
url: "/payment",
|
url: "/",
|
||||||
icon: SquareTerminal,
|
icon: SquareTerminal,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -31,7 +30,7 @@ export class NavPaymentController extends Controller<{
|
|||||||
this.#subscriptions.push(
|
this.#subscriptions.push(
|
||||||
await payment
|
await payment
|
||||||
.collection("beneficiary")
|
.collection("beneficiary")
|
||||||
.subscribe({ tenantId: await User.getTenantId() }, { sort: { label: 1 } }, (beneficiaries) => {
|
.subscribe({ tenantId: auth.user.tenant.id }, { sort: { label: 1 } }, (beneficiaries) => {
|
||||||
this.setState("items", this.#getMenuItems(beneficiaries));
|
this.setState("items", this.#getMenuItems(beneficiaries));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -41,7 +40,7 @@ export class NavPaymentController extends Controller<{
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
url: "/payment",
|
url: "/",
|
||||||
icon: SquareTerminal,
|
icon: SquareTerminal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -57,7 +56,7 @@ export class NavPaymentController extends Controller<{
|
|||||||
},
|
},
|
||||||
...beneficiaries.map((beneficiary) => ({
|
...beneficiaries.map((beneficiary) => ({
|
||||||
title: beneficiary.label ?? "Unlabeled",
|
title: beneficiary.label ?? "Unlabeled",
|
||||||
url: `/payment/${beneficiary._id}`,
|
url: `/beneficiaries/${beneficiary._id}`,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="field-group"
|
data-slot="field-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
"group/field-group @container/field-group flex w-full flex-col gap-3 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { Beneficiary } from "@module/payment/client";
|
|||||||
|
|
||||||
import { loadBeneficiaries, payment } from "@/database/payment.ts";
|
import { loadBeneficiaries, payment } from "@/database/payment.ts";
|
||||||
import { Controller } from "@/lib/controller.tsx";
|
import { Controller } from "@/lib/controller.tsx";
|
||||||
import { User } from "@/services/user.ts";
|
import { auth } from "@/services/auth.ts";
|
||||||
|
|
||||||
export class PaymentDashboardController extends Controller<{
|
export class DashboardController extends Controller<{
|
||||||
beneficiaries: Beneficiary[];
|
beneficiaries: Beneficiary[];
|
||||||
}> {
|
}> {
|
||||||
#subscriptions: any[] = [];
|
#subscriptions: any[] = [];
|
||||||
@@ -28,7 +28,7 @@ export class PaymentDashboardController extends Controller<{
|
|||||||
this.#subscriptions.push(
|
this.#subscriptions.push(
|
||||||
await payment
|
await payment
|
||||||
.collection("beneficiary")
|
.collection("beneficiary")
|
||||||
.subscribe({ tenantId: await User.getTenantId() }, { sort: { label: 1 } }, (documents) => {
|
.subscribe({ tenantId: auth.user.tenant.id }, { sort: { label: 1 } }, (documents) => {
|
||||||
this.setState("beneficiaries", documents);
|
this.setState("beneficiaries", documents);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1,43 +1,89 @@
|
|||||||
import type { Account, Beneficiary, Ledger, Transaction, Wallet } from "@module/payment/client";
|
import {
|
||||||
import { IndexedDatabase } from "@valkyr/db";
|
AccountSchema,
|
||||||
|
BeneficiarySchema,
|
||||||
|
LedgerSchema,
|
||||||
|
TransactionSchema,
|
||||||
|
WalletSchema,
|
||||||
|
} from "@module/payment/client";
|
||||||
|
import { IndexedDB } from "@valkyr/db";
|
||||||
|
|
||||||
import { api } from "@/services/api.ts";
|
import { api } from "@/services/api.ts";
|
||||||
|
|
||||||
export const payment = new IndexedDatabase<{
|
export const payment = new IndexedDB({
|
||||||
account: Account;
|
|
||||||
beneficiary: Beneficiary;
|
|
||||||
ledger: Ledger;
|
|
||||||
transaction: Transaction;
|
|
||||||
wallet: Wallet;
|
|
||||||
}>({
|
|
||||||
name: "valkyr-sandbox:payment",
|
name: "valkyr-sandbox:payment",
|
||||||
registrars: [
|
registrars: [
|
||||||
{
|
{
|
||||||
name: "account",
|
name: "account",
|
||||||
indexes: [["_id", { unique: true }], ["walletId"]],
|
schema: AccountSchema.shape,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
field: "_id",
|
||||||
|
kind: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "walletId",
|
||||||
|
kind: "shared",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "beneficiary",
|
name: "beneficiary",
|
||||||
indexes: [["_id", { unique: true }], ["tenantId"]],
|
schema: BeneficiarySchema.shape,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
field: "_id",
|
||||||
|
kind: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tenantId",
|
||||||
|
kind: "shared",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ledger",
|
name: "ledger",
|
||||||
indexes: [["_id", { unique: true }], ["beneficiaryId"]],
|
schema: LedgerSchema.shape,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
field: "_id",
|
||||||
|
kind: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "beneficiaryId",
|
||||||
|
kind: "shared",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "transaction",
|
name: "transaction",
|
||||||
indexes: [["_id", { unique: true }]],
|
schema: TransactionSchema.shape,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
field: "_id",
|
||||||
|
kind: "primary",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wallet",
|
name: "wallet",
|
||||||
indexes: [["_id", { unique: true }], ["ledgerId"]],
|
schema: WalletSchema.shape,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
field: "_id",
|
||||||
|
kind: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "ledgerId",
|
||||||
|
kind: "shared",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
] as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function loadBeneficiaries(): Promise<void> {
|
export async function loadBeneficiaries(): Promise<void> {
|
||||||
const result = await api.payment.benficiaries.list();
|
const result = await api.payment.benficiaries.list();
|
||||||
if ("data" in result) {
|
if ("data" in result) {
|
||||||
payment.collection("beneficiary").insertMany(result.data);
|
payment.collection("beneficiary").insert(result.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export abstract class Controller<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const state = await this.onInit();
|
const state = await this.onInit();
|
||||||
if (this.#destroyed === false) {
|
if (this.#destroyed === false && state !== undefined) {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
this.#initiated = true;
|
this.#initiated = true;
|
||||||
@@ -64,10 +64,7 @@ export abstract class Controller<
|
|||||||
if (this.onResolve === undefined) {
|
if (this.onResolve === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const state: Partial<TState> = await this.onResolve();
|
await this.onResolve();
|
||||||
if (this.#destroyed === false) {
|
|
||||||
this.setState({ ...this.state, ...state });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $destroy(): Promise<void> {
|
async $destroy(): Promise<void> {
|
||||||
@@ -84,16 +81,12 @@ export abstract class Controller<
|
|||||||
/**
|
/**
|
||||||
* Called every time props change (including first mount).
|
* Called every time props change (including first mount).
|
||||||
*/
|
*/
|
||||||
async onInit(): Promise<Partial<TState>> {
|
async onInit(): Promise<Partial<TState> | void> {}
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called every time props change (including first mount).
|
* Called every time props change (including first mount).
|
||||||
*/
|
*/
|
||||||
async onResolve(): Promise<Partial<TState>> {
|
async onResolve(): Promise<void> {}
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the controller is destroyed.
|
* Called when the controller is destroyed.
|
||||||
|
|||||||
28
app/src/lib/router.ts
Normal file
28
app/src/lib/router.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { auth } from "@/services/auth.ts";
|
||||||
|
|
||||||
|
import { routeTree } from "../routeTree.gen.ts";
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
routeTree,
|
||||||
|
defaultPreload: "intent",
|
||||||
|
scrollRestoration: true,
|
||||||
|
context: {
|
||||||
|
auth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getRouteParam(key: string): string {
|
||||||
|
return getRouteParams()[key] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRouteParams(): Record<string, string> {
|
||||||
|
return router.state.matches.at(-1)?.params ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { createRootRoute, createRoute, createRouter, redirect } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
import { AppView } from "@/app/app.view.tsx";
|
|
||||||
import { CallbackView } from "@/app/auth/callback.view.tsx";
|
|
||||||
import { LoginView } from "@/app/auth/login.view.tsx";
|
|
||||||
import { DashboardView } from "@/app/dashboard/dashboard.view.tsx";
|
|
||||||
import { zitadel } from "@/services/zitadel.ts";
|
|
||||||
|
|
||||||
import { PaymentDashboardView } from "../app/payment/dashboard/dashboard.view.tsx";
|
|
||||||
|
|
||||||
const root = createRootRoute();
|
|
||||||
|
|
||||||
const callback = createRoute({
|
|
||||||
getParentRoute: () => root,
|
|
||||||
path: "/auth/callback",
|
|
||||||
component: CallbackView,
|
|
||||||
});
|
|
||||||
|
|
||||||
const login = createRoute({
|
|
||||||
getParentRoute: () => root,
|
|
||||||
path: "/login",
|
|
||||||
component: LoginView,
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = createRoute({
|
|
||||||
id: "app",
|
|
||||||
getParentRoute: () => root,
|
|
||||||
beforeLoad: async () => {
|
|
||||||
const user = await zitadel.userManager.getUser();
|
|
||||||
if (user === null) {
|
|
||||||
throw redirect({ to: "/login" });
|
|
||||||
}
|
|
||||||
if (user.expired === true) {
|
|
||||||
throw redirect({ to: "/login" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
component: AppView,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dashboard = createRoute({
|
|
||||||
getParentRoute: () => app,
|
|
||||||
path: "/",
|
|
||||||
component: DashboardView,
|
|
||||||
});
|
|
||||||
|
|
||||||
const payment = [
|
|
||||||
createRoute({
|
|
||||||
getParentRoute: () => app,
|
|
||||||
path: "/payment",
|
|
||||||
component: PaymentDashboardView,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
root.addChildren([app, login, callback]);
|
|
||||||
app.addChildren([dashboard, ...payment]);
|
|
||||||
|
|
||||||
export const router = createRouter({ routeTree: root });
|
|
||||||
@@ -3,14 +3,9 @@ import { StrictMode } from "react";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider.tsx";
|
import { ThemeProvider } from "@/components/theme-provider.tsx";
|
||||||
import { router } from "@/lib/router.tsx";
|
import { router } from "@/lib/router";
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
import "./index.css";
|
||||||
interface Register {
|
|
||||||
router: typeof router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootElement = document.getElementById("root");
|
const rootElement = document.getElementById("root");
|
||||||
if (rootElement === null) {
|
if (rootElement === null) {
|
||||||
|
|||||||
126
app/src/routeTree.gen.ts
Normal file
126
app/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
|
import { Route as CallbackRouteImport } from './routes/callback'
|
||||||
|
import { Route as AuthRouteRouteImport } from './routes/_auth/route'
|
||||||
|
import { Route as AuthIndexRouteImport } from './routes/_auth/index'
|
||||||
|
import { Route as AuthBeneficiariesBeneficiaryIdRouteImport } from './routes/_auth/beneficiaries/$beneficiaryId'
|
||||||
|
|
||||||
|
const CallbackRoute = CallbackRouteImport.update({
|
||||||
|
id: '/callback',
|
||||||
|
path: '/callback',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AuthRouteRoute = AuthRouteRouteImport.update({
|
||||||
|
id: '/_auth',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const AuthIndexRoute = AuthIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => AuthRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthBeneficiariesBeneficiaryIdRoute =
|
||||||
|
AuthBeneficiariesBeneficiaryIdRouteImport.update({
|
||||||
|
id: '/beneficiaries/$beneficiaryId',
|
||||||
|
path: '/beneficiaries/$beneficiaryId',
|
||||||
|
getParentRoute: () => AuthRouteRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/callback': typeof CallbackRoute
|
||||||
|
'/': typeof AuthIndexRoute
|
||||||
|
'/beneficiaries/$beneficiaryId': typeof AuthBeneficiariesBeneficiaryIdRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/callback': typeof CallbackRoute
|
||||||
|
'/': typeof AuthIndexRoute
|
||||||
|
'/beneficiaries/$beneficiaryId': typeof AuthBeneficiariesBeneficiaryIdRoute
|
||||||
|
}
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRouteImport
|
||||||
|
'/_auth': typeof AuthRouteRouteWithChildren
|
||||||
|
'/callback': typeof CallbackRoute
|
||||||
|
'/_auth/': typeof AuthIndexRoute
|
||||||
|
'/_auth/beneficiaries/$beneficiaryId': typeof AuthBeneficiariesBeneficiaryIdRoute
|
||||||
|
}
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths: '/callback' | '/' | '/beneficiaries/$beneficiaryId'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to: '/callback' | '/' | '/beneficiaries/$beneficiaryId'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/_auth'
|
||||||
|
| '/callback'
|
||||||
|
| '/_auth/'
|
||||||
|
| '/_auth/beneficiaries/$beneficiaryId'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
AuthRouteRoute: typeof AuthRouteRouteWithChildren
|
||||||
|
CallbackRoute: typeof CallbackRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/callback': {
|
||||||
|
id: '/callback'
|
||||||
|
path: '/callback'
|
||||||
|
fullPath: '/callback'
|
||||||
|
preLoaderRoute: typeof CallbackRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_auth': {
|
||||||
|
id: '/_auth'
|
||||||
|
path: ''
|
||||||
|
fullPath: ''
|
||||||
|
preLoaderRoute: typeof AuthRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/_auth/': {
|
||||||
|
id: '/_auth/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof AuthIndexRouteImport
|
||||||
|
parentRoute: typeof AuthRouteRoute
|
||||||
|
}
|
||||||
|
'/_auth/beneficiaries/$beneficiaryId': {
|
||||||
|
id: '/_auth/beneficiaries/$beneficiaryId'
|
||||||
|
path: '/beneficiaries/$beneficiaryId'
|
||||||
|
fullPath: '/beneficiaries/$beneficiaryId'
|
||||||
|
preLoaderRoute: typeof AuthBeneficiariesBeneficiaryIdRouteImport
|
||||||
|
parentRoute: typeof AuthRouteRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthRouteRouteChildren {
|
||||||
|
AuthIndexRoute: typeof AuthIndexRoute
|
||||||
|
AuthBeneficiariesBeneficiaryIdRoute: typeof AuthBeneficiariesBeneficiaryIdRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthRouteRouteChildren: AuthRouteRouteChildren = {
|
||||||
|
AuthIndexRoute: AuthIndexRoute,
|
||||||
|
AuthBeneficiariesBeneficiaryIdRoute: AuthBeneficiariesBeneficiaryIdRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthRouteRouteWithChildren = AuthRouteRoute._addFileChildren(
|
||||||
|
AuthRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
AuthRouteRoute: AuthRouteRouteWithChildren,
|
||||||
|
CallbackRoute: CallbackRoute,
|
||||||
|
}
|
||||||
|
export const routeTree = rootRouteImport
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
19
app/src/routes/__root.tsx
Normal file
19
app/src/routes/__root.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
|
||||||
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
|
|
||||||
|
import type { AuthContext } from "@/services/auth.ts";
|
||||||
|
|
||||||
|
export const Route = createRootRouteWithContext<{
|
||||||
|
auth: AuthContext;
|
||||||
|
}>()({
|
||||||
|
component: RootComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RootComponent() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Outlet />
|
||||||
|
<TanStackRouterDevtools position="bottom-right" initialIsOpen={false} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/src/routes/_auth/beneficiaries/$beneficiaryId.tsx
Normal file
12
app/src/routes/_auth/beneficiaries/$beneficiaryId.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { BeneficiaryComponent } from "@/components/beneficiary/read.component.tsx";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/beneficiaries/$beneficiaryId")({
|
||||||
|
component: BeneficiaryRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BeneficiaryRoute() {
|
||||||
|
const { beneficiaryId } = Route.useParams();
|
||||||
|
return <BeneficiaryComponent id={beneficiaryId} />;
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { DashboardController } from "@/controllers/dashboard.controller.ts";
|
||||||
import { makeControllerComponent } from "@/lib/controller.tsx";
|
import { makeControllerComponent } from "@/lib/controller.tsx";
|
||||||
|
|
||||||
import { PaymentDashboardController } from "./dashboard.controller.ts";
|
const DashboardComponent = makeControllerComponent(DashboardController, ({ beneficiaries }) => {
|
||||||
|
|
||||||
export const PaymentDashboardView = makeControllerComponent(PaymentDashboardController, ({ beneficiaries }) => {
|
|
||||||
if (beneficiaries.length === 0) {
|
if (beneficiaries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col pt-20 items-center justify-center text-center">
|
<div className="w-full flex flex-col pt-20 items-center justify-center text-center">
|
||||||
@@ -20,3 +21,7 @@ export const PaymentDashboardView = makeControllerComponent(PaymentDashboardCont
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/")({
|
||||||
|
component: DashboardComponent,
|
||||||
|
});
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { Outlet } from "@tanstack/react-router";
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { AppSidebar } from "@/components/app-sidebar";
|
import { AppSidebar } from "@/components/app-sidebar";
|
||||||
import { SiteHeader } from "@/components/site-header";
|
import { SiteHeader } from "@/components/site-header";
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export function AppView() {
|
export const Route = createFileRoute("/_auth")({
|
||||||
|
beforeLoad: async ({ context: { auth } }) => {
|
||||||
|
await auth.resolve();
|
||||||
|
if (auth.isAuthenticated === false) {
|
||||||
|
throw auth.login();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
component: AppLayout,
|
||||||
|
});
|
||||||
|
|
||||||
|
function AppLayout() {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
style={
|
style={
|
||||||
35
app/src/routes/callback.tsx
Normal file
35
app/src/routes/callback.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { zitadel } from "@/services/zitadel.ts";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/callback")({
|
||||||
|
validateSearch: z.object({
|
||||||
|
code: z.string(),
|
||||||
|
}),
|
||||||
|
component: AuthComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function AuthComponent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
zitadel.userManager
|
||||||
|
.signinRedirectCallback()
|
||||||
|
.then(() => {
|
||||||
|
navigate({ to: "/", replace: true });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Callback error", error);
|
||||||
|
setError(error);
|
||||||
|
});
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
32
app/src/services/auth.ts
Normal file
32
app/src/services/auth.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { User } from "@/services/user.ts";
|
||||||
|
import { zitadel } from "@/services/zitadel.ts";
|
||||||
|
|
||||||
|
export const auth: AuthContext = {
|
||||||
|
get isAuthenticated() {
|
||||||
|
return User.isAuthenticated;
|
||||||
|
},
|
||||||
|
|
||||||
|
get user() {
|
||||||
|
return User.instance;
|
||||||
|
},
|
||||||
|
|
||||||
|
async resolve() {
|
||||||
|
await User.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
async login() {
|
||||||
|
await zitadel.authorize();
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
await zitadel.signout();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthContext = {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
user: User;
|
||||||
|
resolve: () => Promise<void>;
|
||||||
|
login: () => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { redirect } from "@tanstack/react-router";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { type ZitadelUser, zitadel } from "./zitadel.ts";
|
import { type ZitadelUser, zitadel } from "./zitadel.ts";
|
||||||
@@ -35,20 +34,23 @@ export class User {
|
|||||||
this.tenant = tenant;
|
this.tenant = tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getTenantId() {
|
static get isAuthenticated() {
|
||||||
return User.get().then((user) => user.tenant.id);
|
return cached !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getTenantName() {
|
static get instance() {
|
||||||
return User.get().then((user) => user.tenant.name);
|
if (cached === undefined) {
|
||||||
|
throw zitadel.authorize();
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async get() {
|
static get tenantId() {
|
||||||
const user = await User.resolve();
|
return User.instance.tenant.id;
|
||||||
if (user === undefined) {
|
|
||||||
throw redirect({ to: "/login" });
|
|
||||||
}
|
}
|
||||||
return user;
|
|
||||||
|
static get tenantName() {
|
||||||
|
return User.instance.tenant.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resolve() {
|
static async resolve() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const config: ZitadelConfig = {
|
|||||||
authority: "https://iam.valkyrjs.com",
|
authority: "https://iam.valkyrjs.com",
|
||||||
project_resource_id: "348389288439709700",
|
project_resource_id: "348389288439709700",
|
||||||
client_id: "348389308220112900",
|
client_id: "348389308220112900",
|
||||||
redirect_uri: "http://localhost:5173/auth/callback",
|
redirect_uri: "http://localhost:5173/callback",
|
||||||
post_logout_redirect_uri: "http://localhost:5173",
|
post_logout_redirect_uri: "http://localhost:5173",
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: "openid profile email urn:zitadel:iam:user:metadata urn:zitadel:iam:org:id:348388915649970180",
|
scope: "openid profile email urn:zitadel:iam:user:metadata urn:zitadel:iam:org:id:348388915649970180",
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
tanstackRouter({
|
||||||
|
target: "react",
|
||||||
|
autoCodeSplitting: true,
|
||||||
|
}),
|
||||||
|
react(),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"tailwindcss": "4.1.13",
|
"tailwindcss": "4.1.13",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.35.0",
|
"@eslint/js": "9.35.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"formatWithErrors": false,
|
"formatWithErrors": false,
|
||||||
|
|||||||
@@ -12,34 +12,34 @@ services:
|
|||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# Used by event store and read store for managing and reading application data.
|
# Used by event store and read store for managing and reading application data.
|
||||||
|
|
||||||
mongo:
|
# mongo:
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
image: mongo:8
|
# image: mongo:8
|
||||||
container_name: boilerplate_mongo
|
# container_name: boilerplate_mongo
|
||||||
ports:
|
# ports:
|
||||||
- 6017:27017
|
# - 6017:27017
|
||||||
environment:
|
# environment:
|
||||||
MONGO_INITDB_ROOT_USERNAME: root
|
# MONGO_INITDB_ROOT_USERNAME: root
|
||||||
MONGO_INITDB_ROOT_PASSWORD: password
|
# MONGO_INITDB_ROOT_PASSWORD: password
|
||||||
volumes:
|
# volumes:
|
||||||
- mongo:/data/db
|
# - mongo:/data/db
|
||||||
networks:
|
# networks:
|
||||||
- server
|
# - server
|
||||||
|
|
||||||
# Cerbos
|
# Cerbos
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# Policy engine for application access control.
|
# Policy engine for application access control.
|
||||||
|
|
||||||
cerbos:
|
# cerbos:
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
image: ghcr.io/cerbos/cerbos:latest
|
# image: ghcr.io/cerbos/cerbos:latest
|
||||||
container_name: boilerplate_cerbos
|
# container_name: boilerplate_cerbos
|
||||||
command: ["server", "--config=/config.yaml"]
|
# command: ["server", "--config=/config.yaml"]
|
||||||
ports:
|
# ports:
|
||||||
- 6592:3592
|
# - 6592:3592
|
||||||
- 6593:3593
|
# - 6593:3593
|
||||||
- 6594:3594
|
# - 6594:3594
|
||||||
volumes:
|
# volumes:
|
||||||
- ./cerbos.yaml:/config.yaml
|
# - ./cerbos.yaml:/config.yaml
|
||||||
networks:
|
# networks:
|
||||||
- server
|
# - server
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@platform/relay": "workspace:*",
|
"@platform/relay": "workspace:*",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"@platform/database": "workspace:*",
|
"@platform/database": "workspace:*",
|
||||||
"@platform/parse": "workspace:*",
|
"@platform/parse": "workspace:*",
|
||||||
"@platform/relay": "workspace:*",
|
"@platform/relay": "workspace:*",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,47 +7,72 @@ import { type Account, type AccountInsert, AccountInsertSchema, AccountSchema }
|
|||||||
* Create a new account.
|
* Create a new account.
|
||||||
*
|
*
|
||||||
* @param values - Account values to insert.
|
* @param values - Account values to insert.
|
||||||
|
*
|
||||||
|
* @returns Account _id
|
||||||
*/
|
*/
|
||||||
export async function createAccount(values: AccountInsert): Promise<string> {
|
export async function createAccount(values: AccountInsert): Promise<string> {
|
||||||
return db
|
return db
|
||||||
.begin(async () => {
|
.begin(async () => {
|
||||||
const _id = crypto.randomUUID();
|
const _id = crypto.randomUUID();
|
||||||
|
|
||||||
// Assert wallet exists
|
// ### Assert Wallet
|
||||||
|
// Ensure that the wallet we are creating an account for exists.
|
||||||
|
|
||||||
await db.sql`
|
await db.sql`
|
||||||
ASSERT EXISTS (
|
ASSERT EXISTS (
|
||||||
SELECT 1
|
SELECT
|
||||||
FROM payment.wallet w
|
1
|
||||||
WHERE w._id = ${db.text(values.walletId)}
|
FROM
|
||||||
), 'missing_wallet';
|
payment.wallet wallet
|
||||||
|
WHERE
|
||||||
|
wallet._id = ${db.text(values.walletId)}
|
||||||
|
),
|
||||||
|
'missing_wallet';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Assert wallet → ledger relationship exists AND ledger exists
|
// ### Assert Wallet → Ledger
|
||||||
|
// Ensure that the wallet is related to a existing ledger.
|
||||||
|
|
||||||
await db.sql`
|
await db.sql`
|
||||||
ASSERT EXISTS (
|
ASSERT EXISTS (
|
||||||
SELECT 1
|
SELECT
|
||||||
FROM payment.wallet w
|
1
|
||||||
JOIN payment.ledger l ON l._id = w."ledgerId"
|
FROM
|
||||||
WHERE w._id = ${db.text(values.walletId)}
|
payment.wallet wallet
|
||||||
), 'missing_ledger';
|
JOIN
|
||||||
|
payment.ledger ledger ON ledger._id = wallet."ledgerId"
|
||||||
|
WHERE
|
||||||
|
wallet._id = ${db.text(values.walletId)}
|
||||||
|
),
|
||||||
|
'missing_ledger';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Assert ledger supports the currency
|
// ### Assert Currency
|
||||||
|
// Ensure that the account currency is supported by the ledger.
|
||||||
|
|
||||||
await db.sql`
|
await db.sql`
|
||||||
ASSERT EXISTS (
|
ASSERT EXISTS (
|
||||||
SELECT 1
|
SELECT
|
||||||
FROM payment.wallet w
|
1
|
||||||
JOIN payment.ledger l ON l._id = w."ledgerId"
|
FROM
|
||||||
WHERE w._id = ${db.text(values.walletId)}
|
payment.wallet wallet
|
||||||
AND ${db.text(values.currency)} IN (
|
JOIN
|
||||||
|
payment.ledger ledger ON ledger._id = wallet."ledgerId"
|
||||||
|
WHERE
|
||||||
|
wallet._id = ${db.text(values.walletId)}
|
||||||
|
AND
|
||||||
|
${db.text(values.currency)} IN (
|
||||||
SELECT
|
SELECT
|
||||||
currency
|
currency
|
||||||
FROM
|
FROM
|
||||||
UNNEST(l.currencies) AS x(currency)
|
UNNEST(ledger.currencies) AS x(currency)
|
||||||
)
|
)
|
||||||
), 'unsupported_currency';
|
),
|
||||||
|
'unsupported_currency';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// ### Create Account
|
||||||
|
|
||||||
await db.sql`INSERT INTO payment.account RECORDS ${db.transit({ _id, ...AccountInsertSchema.parse(values) })}`;
|
await db.sql`INSERT INTO payment.account RECORDS ${db.transit({ _id, ...AccountInsertSchema.parse(values) })}`;
|
||||||
|
|
||||||
return _id;
|
return _id;
|
||||||
@@ -71,6 +96,13 @@ export async function createAccount(values: AccountInsert): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all accounts registered for a wallet.
|
||||||
|
*
|
||||||
|
* @param walletId - Wallet to fetch accounts from.
|
||||||
|
*
|
||||||
|
* @returns List of wallet accounts
|
||||||
|
*/
|
||||||
export async function getAccountsByWalletId(walletId: string): Promise<Account[]> {
|
export async function getAccountsByWalletId(walletId: string): Promise<Account[]> {
|
||||||
return db.schema(AccountSchema).many`
|
return db.schema(AccountSchema).many`
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export async function getBeneficiaryByTenantId(tenantId: string): Promise<Benefi
|
|||||||
* Get a beneficiary entity by provided id.
|
* Get a beneficiary entity by provided id.
|
||||||
*
|
*
|
||||||
* @param id - Identity of the beneficiary to retrieve.
|
* @param id - Identity of the beneficiary to retrieve.
|
||||||
|
*
|
||||||
|
* @returns Beneficiary or undefined
|
||||||
*/
|
*/
|
||||||
export async function getBeneficiaryById(id: string): Promise<Beneficiary | undefined> {
|
export async function getBeneficiaryById(id: string): Promise<Beneficiary | undefined> {
|
||||||
return db.schema(BeneficiarySchema).one`
|
return db.schema(BeneficiarySchema).one`
|
||||||
|
|||||||
@@ -7,12 +7,31 @@ import { type Ledger, type LedgerInsert, LedgerSchema } from "../schemas/ledger.
|
|||||||
* Create a new ledger.
|
* Create a new ledger.
|
||||||
*
|
*
|
||||||
* @param values - Ledger values to insert.
|
* @param values - Ledger values to insert.
|
||||||
|
*
|
||||||
|
* @returns Ledger _id
|
||||||
*/
|
*/
|
||||||
export async function createLedger(values: LedgerInsert): Promise<string> {
|
export async function createLedger(values: LedgerInsert): Promise<string> {
|
||||||
return db
|
return db
|
||||||
.begin(async () => {
|
.begin(async () => {
|
||||||
const _id = crypto.randomUUID();
|
const _id = crypto.randomUUID();
|
||||||
await db.sql`ASSERT EXISTS (SELECT 1 FROM payment.beneficiary WHERE _id = ${db.text(values.beneficiaryId)}), 'missing_beneficiary'`;
|
|
||||||
|
// ### Assert Beneficiary
|
||||||
|
// Ensure the beneficiary for the ledger exists.
|
||||||
|
|
||||||
|
await db.sql`
|
||||||
|
ASSERT EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
payment.beneficiary
|
||||||
|
WHERE
|
||||||
|
_id = ${db.text(values.beneficiaryId)}
|
||||||
|
),
|
||||||
|
'missing_beneficiary'
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ### Create Ledger
|
||||||
|
|
||||||
await db.sql`
|
await db.sql`
|
||||||
INSERT INTO payment.ledger (
|
INSERT INTO payment.ledger (
|
||||||
_id,
|
_id,
|
||||||
@@ -26,6 +45,7 @@ export async function createLedger(values: LedgerInsert): Promise<string> {
|
|||||||
${values.label ? db.text(values.label) : null}
|
${values.label ? db.text(values.label) : null}
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return _id;
|
return _id;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -7,13 +7,33 @@ import { type Wallet, type WalletInsert, WalletInsertSchema, WalletSchema } from
|
|||||||
* Create a new wallet.
|
* Create a new wallet.
|
||||||
*
|
*
|
||||||
* @param values - Wallet values to insert.
|
* @param values - Wallet values to insert.
|
||||||
|
*
|
||||||
|
* @returns Wallet _id
|
||||||
*/
|
*/
|
||||||
export async function createWallet(values: WalletInsert): Promise<string> {
|
export async function createWallet(values: WalletInsert): Promise<string> {
|
||||||
return db
|
return db
|
||||||
.begin(async () => {
|
.begin(async () => {
|
||||||
const _id = crypto.randomUUID();
|
const _id = crypto.randomUUID();
|
||||||
await db.sql`ASSERT EXISTS (SELECT 1 FROM payment.ledger WHERE _id = ${db.text(values.ledgerId)}), 'missing_ledger'`;
|
|
||||||
|
// ### Assert Ledger
|
||||||
|
// Ensure the ledger for the wallet exists.
|
||||||
|
|
||||||
|
await db.sql`
|
||||||
|
ASSERT EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
payment.ledger
|
||||||
|
WHERE
|
||||||
|
_id = ${db.text(values.ledgerId)}
|
||||||
|
),
|
||||||
|
'missing_ledger'
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ### Create Wallet
|
||||||
|
|
||||||
await db.sql`INSERT INTO payment.wallet RECORDS ${db.transit({ _id, ...WalletInsertSchema.parse(values) })}`;
|
await db.sql`INSERT INTO payment.wallet RECORDS ${db.transit({ _id, ...WalletInsertSchema.parse(values) })}`;
|
||||||
|
|
||||||
return _id;
|
return _id;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
"@platform/database": "workspace:*",
|
"@platform/database": "workspace:*",
|
||||||
"@platform/parse": "workspace:*",
|
"@platform/parse": "workspace:*",
|
||||||
"@platform/relay": "workspace:*",
|
"@platform/relay": "workspace:*",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@std/dotenv": "npm:@jsr/std__dotenv@0.225.5",
|
"@std/dotenv": "npm:@jsr/std__dotenv@0.225.5",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@
|
|||||||
"@types/transit-js": "0.8.3",
|
"@types/transit-js": "0.8.3",
|
||||||
"postgres": "3.4.7",
|
"postgres": "3.4.7",
|
||||||
"transit-js": "0.8.874",
|
"transit-js": "0.8.874",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@platform/config": "workspace:*",
|
"@platform/config": "workspace:*",
|
||||||
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
|
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
".": "./mod.ts"
|
".": "./mod.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"@platform/socket": "workspace:*",
|
"@platform/socket": "workspace:*",
|
||||||
"@platform/supertokens": "workspace:*",
|
"@platform/supertokens": "workspace:*",
|
||||||
"path-to-regexp": "8",
|
"path-to-regexp": "8",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@platform/relay": "workspace:*",
|
"@platform/relay": "workspace:*",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
"@platform/socket": "workspace:*",
|
"@platform/socket": "workspace:*",
|
||||||
"@platform/storage": "workspace:*",
|
"@platform/storage": "workspace:*",
|
||||||
"@valkyr/json-rpc": "npm:@jsr/valkyr__json-rpc@1.1.0",
|
"@valkyr/json-rpc": "npm:@jsr/valkyr__json-rpc@1.1.0",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@platform/models": "workspace:*",
|
"@platform/models": "workspace:*",
|
||||||
"@platform/relay": "workspace:*",
|
"@platform/relay": "workspace:*",
|
||||||
"zod": "4.1.13"
|
"zod": "4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user