feat: docs
This commit is contained in:
parent
6ab00206df
commit
d382485a7c
@ -1,6 +1,7 @@
|
||||
.direnv
|
||||
.env
|
||||
build
|
||||
result
|
||||
|
||||
# Client
|
||||
/client/node_modules
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.direnv
|
||||
.env
|
||||
build
|
||||
result
|
||||
|
||||
# Client
|
||||
/client/node_modules
|
||||
|
3947
client/package-lock.json
generated
3947
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,11 +18,14 @@
|
||||
"@connectrpc/connect-web": "^2.0.2",
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@lucide/svelte": "^0.479.0",
|
||||
"@scalar/api-reference": "^1.28.1",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.13",
|
||||
"bits-ui": "^1.3.11",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
@ -32,7 +35,10 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.13",
|
||||
"tw-animate-css": "^1.2.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.0"
|
||||
|
@ -1 +1,22 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@theme {
|
||||
--color-crust: #11111b;
|
||||
--color-mantle: #181825;
|
||||
--color-base: #1e1e2e;
|
||||
|
||||
--color-surface-0: #313244;
|
||||
--color-surface-1: #45475a;
|
||||
--color-surface-2: #585b70;
|
||||
|
||||
--color-overlay-0: #6c7086;
|
||||
--color-overlay-1: #7f849c;
|
||||
--color-overlay-2: #9399b2;
|
||||
|
||||
--color-subtext-0: #a6adc8;
|
||||
--color-subtext-1: #bac2de;
|
||||
|
||||
--color-text: #cdd6f4;
|
||||
--color-sky: #89dceb;
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="tap" class="min-h-screen bg-base text-text">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file user/v1/auth.proto.
|
||||
*/
|
||||
export const file_user_v1_auth: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiMwoNU2lnblVwUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIQCg5TaWduVXBSZXNwb25zZSIPCg1Mb2dvdXRSZXF1ZXN0IhAKDkxvZ291dFJlc3BvbnNlMsEBCgtBdXRoU2VydmljZRI4CgVMb2dpbhIVLnVzZXIudjEuTG9naW5SZXF1ZXN0GhYudXNlci52MS5Mb2dpblJlc3BvbnNlIgASOwoGU2lnblVwEhYudXNlci52MS5TaWduVXBSZXF1ZXN0GhcudXNlci52MS5TaWduVXBSZXNwb25zZSIAEjsKBkxvZ291dBIWLnVzZXIudjEuTG9nb3V0UmVxdWVzdBoXLnVzZXIudjEuTG9nb3V0UmVzcG9uc2UiAEKdAQoLY29tLnVzZXIudjFCCUF1dGhQcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL3VzZXIvdjE7dXNlcnYxogIDVVhYqgIHVXNlci5WMcoCB1VzZXJcVjHiAhNVc2VyXFYxXEdQQk1ldGFkYXRh6gIIVXNlcjo6VjFiBnByb3RvMw");
|
||||
fileDesc("ChJ1c2VyL3YxL2F1dGgucHJvdG8SB3VzZXIudjEiMgoMTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIh4KDUxvZ2luUmVzcG9uc2USDQoFdG9rZW4YASABKAkiTQoNU2lnblVwUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCRIYChBjb25maXJtX3Bhc3N3b3JkGAMgASgJIhAKDlNpZ25VcFJlc3BvbnNlIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UywQEKC0F1dGhTZXJ2aWNlEjgKBUxvZ2luEhUudXNlci52MS5Mb2dpblJlcXVlc3QaFi51c2VyLnYxLkxvZ2luUmVzcG9uc2UiABI7CgZTaWduVXASFi51c2VyLnYxLlNpZ25VcFJlcXVlc3QaFy51c2VyLnYxLlNpZ25VcFJlc3BvbnNlIgASOwoGTG9nb3V0EhYudXNlci52MS5Mb2dvdXRSZXF1ZXN0GhcudXNlci52MS5Mb2dvdXRSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJQXV0aFByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z");
|
||||
|
||||
/**
|
||||
* @generated from message user.v1.LoginRequest
|
||||
@ -64,6 +64,11 @@ export type SignUpRequest = Message<"user.v1.SignUpRequest"> & {
|
||||
* @generated from field: string password = 2;
|
||||
*/
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string confirm_password = 3;
|
||||
*/
|
||||
confirmPassword: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
6
client/src/lib/utils.ts
Normal file
6
client/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
189
client/src/routes/(app)/+layout.svelte
Normal file
189
client/src/routes/(app)/+layout.svelte
Normal file
@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
LayoutGrid,
|
||||
Settings,
|
||||
LogOut,
|
||||
Menu,
|
||||
LayoutList,
|
||||
Book,
|
||||
House,
|
||||
type Icon as IconType
|
||||
} from '@lucide/svelte';
|
||||
import { NavigationMenu, Popover, Separator, Dialog } from 'bits-ui';
|
||||
import { fly, slide } from 'svelte/transition';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AuthClient } from '$lib/transport';
|
||||
import { page } from '$app/state';
|
||||
import { cn } from '$lib/utils';
|
||||
let { children } = $props();
|
||||
|
||||
const username = localStorage.getItem('username');
|
||||
let sidebarOpen = $state(false);
|
||||
|
||||
type MenuItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: typeof IconType;
|
||||
};
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
name: 'Home',
|
||||
href: '/',
|
||||
icon: House
|
||||
},
|
||||
{
|
||||
name: 'Items',
|
||||
href: '/items',
|
||||
icon: LayoutList
|
||||
},
|
||||
{
|
||||
name: 'Docs',
|
||||
href: '/docs/',
|
||||
icon: Book
|
||||
}
|
||||
];
|
||||
|
||||
async function logout() {
|
||||
await AuthClient.logout({});
|
||||
localStorage.removeItem('username');
|
||||
await goto('/auth');
|
||||
toast.success('logged out successfully');
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="border-surface-0 bg-mantle fixed flex h-[50px] w-full items-center justify-between border-b p-2 px-6 drop-shadow-md"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<Dialog.Root bind:open={sidebarOpen}>
|
||||
<Dialog.Trigger class="hover:bg-surface-0 cursor-pointer rounded p-1 px-3 transition-all">
|
||||
<Menu />
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-50 mt-[50px] bg-black/50" />
|
||||
<Dialog.Content forceMount>
|
||||
{#snippet child({ props, open })}
|
||||
{#if open}
|
||||
<div
|
||||
class="bg-mantle border-surface-0 fixed inset-0 z-50 mt-[50px] flex w-60 flex-col justify-between border-r drop-shadow-md"
|
||||
{...props}
|
||||
transition:slide={{
|
||||
axis: 'x'
|
||||
}}
|
||||
>
|
||||
<NavigationMenu.Root orientation="vertical">
|
||||
<NavigationMenu.List class="flex w-full flex-col gap-2 overflow-y-scroll p-2">
|
||||
{#each menuItems as item}
|
||||
{@const Icon = item.icon}
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Link
|
||||
class={cn(
|
||||
'hover:bg-surface-0 flex select-none gap-2 whitespace-nowrap rounded-lg p-2 transition-all',
|
||||
page.url.pathname === item.href && 'bg-surface-0'
|
||||
)}
|
||||
href={item.href}
|
||||
onclick={() => {
|
||||
if (sidebarOpen) {
|
||||
sidebarOpen = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
<span>{item.name}</span>
|
||||
</NavigationMenu.Link>
|
||||
</NavigationMenu.Item>
|
||||
{/each}
|
||||
</NavigationMenu.List>
|
||||
</NavigationMenu.Root>
|
||||
|
||||
<div class="border-surface-0 flex flex-col gap-2 border-t p-2">
|
||||
<a
|
||||
href="/settings"
|
||||
class="hover:bg-surface-0 flex select-none items-center gap-2 rounded-lg p-2 transition-all"
|
||||
>
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="hover:bg-surface-0 flex w-full cursor-pointer items-center gap-2 whitespace-nowrap rounded-lg p-2 transition-all"
|
||||
onclick={logout}
|
||||
>
|
||||
<LogOut size="20" />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
||||
<a href="/" class="flex select-none items-center gap-2 text-2xl font-bold tracking-wider">
|
||||
TrevStack
|
||||
<LayoutGrid />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<NavigationMenu.Root class="hidden md:block">
|
||||
<NavigationMenu.List class="flex gap-2">
|
||||
{#each menuItems as item}
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Link
|
||||
class={cn(
|
||||
'hover:bg-surface-0 flex select-none gap-2 rounded-lg p-1 px-2 transition-all',
|
||||
page.url.pathname === item.href && 'bg-surface-0'
|
||||
)}
|
||||
href={item.href}
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
</NavigationMenu.Link>
|
||||
</NavigationMenu.Item>
|
||||
{/each}
|
||||
</NavigationMenu.List>
|
||||
<NavigationMenu.Viewport class="absolute" />
|
||||
</NavigationMenu.Root>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
class="border-surface-2 hover:bg-surface-0 cursor-pointer rounded border p-1 px-4 text-sm transition-all"
|
||||
>
|
||||
{username}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content forceMount>
|
||||
{#snippet child({ wrapperProps, props, open })}
|
||||
{#if open}
|
||||
<div {...wrapperProps}>
|
||||
<div
|
||||
class="bg-mantle border-surface-0 z-50 mt-1 rounded border drop-shadow-md transition-all"
|
||||
{...props}
|
||||
transition:fly
|
||||
>
|
||||
<a
|
||||
class="hover:bg-surface-0 flex items-center gap-1 p-3 px-4 text-sm"
|
||||
href="/settings"
|
||||
>
|
||||
<Settings size="20" />
|
||||
Settings
|
||||
</a>
|
||||
<Separator.Root class="bg-surface-0 h-px" />
|
||||
<button
|
||||
class="hover:bg-surface-0 flex w-full cursor-pointer items-center gap-1 p-3 px-4 text-sm transition-all"
|
||||
onclick={logout}
|
||||
>
|
||||
<LogOut size="20" />
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</header>
|
||||
|
||||
<div class="pt-[50px]">
|
||||
{@render children()}
|
||||
</div>
|
@ -1,2 +1,2 @@
|
||||
<h1>Welcome to TrevStack</h1>
|
||||
<p>Visit <a href="https://github.com/spotdemo4/trevstack">github.com/spotdemo4/trevstack</a> to read the documentation</p>
|
||||
<p>Visit <a href="https://github.com/spotdemo4/trevstack">github.com/spotdemo4/trevstack</a> to read the documentation</p>
|
26
client/src/routes/(app)/docs/+page.svelte
Normal file
26
client/src/routes/(app)/docs/+page.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { createApiReference } from '@scalar/api-reference';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let ref: ReturnType<typeof createApiReference>;
|
||||
let htmlref: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
ref = createApiReference(htmlref, {
|
||||
url: '/openapi/openapi.yaml',
|
||||
hideClientButton: true
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
ref.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
@import '@scalar/api-reference/style.css';
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
<div bind:this={htmlref}></div>
|
@ -1,6 +1,14 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
import '../app.css';
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<Toaster toastOptions={{
|
||||
classes: {
|
||||
toast: '!bg-mantle !text-text !border-surface-0',
|
||||
}
|
||||
}} />
|
||||
|
||||
{@render children()}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
export const ssr = false;
|
||||
export const trailingSlash = 'always';
|
@ -1,29 +1,151 @@
|
||||
<script lang="ts">
|
||||
import { Tabs } from 'bits-ui';
|
||||
import { Tabs, Button } from 'bits-ui';
|
||||
import { cn } from '$lib/utils';
|
||||
import { AuthClient } from '$lib/transport';
|
||||
import { goto } from '$app/navigation';
|
||||
import { ConnectError } from '@connectrpc/connect';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let tab = $state('login');
|
||||
</script>
|
||||
|
||||
<div class="pt-6">
|
||||
<Tabs.Root
|
||||
value="outbound"
|
||||
class="rounded-card border-muted bg-background-alt shadow-card w-[390px] border p-3"
|
||||
>
|
||||
<div class="flex h-screen flex-col items-center justify-center">
|
||||
<Tabs.Root bind:value={tab} class="w-[390px] p-3">
|
||||
<Tabs.List
|
||||
class="rounded-9px bg-dark-10 shadow-mini-inset dark:bg-background grid w-full grid-cols-2 gap-1 p-1 text-sm font-semibold leading-[0.01em] dark:border dark:border-neutral-600/30"
|
||||
class="bg-mantle border-surface-0 flex w-full justify-around gap-1 rounded-lg border p-1 drop-shadow-md"
|
||||
>
|
||||
<Tabs.Trigger
|
||||
value="outbound"
|
||||
class="data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white"
|
||||
value="login"
|
||||
class={cn(
|
||||
'hover:bg-surface-0 grow cursor-pointer rounded p-2 transition-all',
|
||||
tab == 'login' && 'bg-surface-0'
|
||||
)}>Log In</Tabs.Trigger
|
||||
>
|
||||
Outbound
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="inbound"
|
||||
class="data-[state=active]:shadow-mini dark:data-[state=active]:bg-muted h-8 rounded-[7px] bg-transparent py-2 data-[state=active]:bg-white"
|
||||
value="signup"
|
||||
class={cn(
|
||||
'hover:bg-surface-0 grow cursor-pointer rounded p-2 transition-all',
|
||||
tab == 'signup' && 'bg-surface-0'
|
||||
)}>Sign Up</Tabs.Trigger
|
||||
>
|
||||
Inbound
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="outbound" class="select-none pt-3">Test1</Tabs.Content>
|
||||
<Tabs.Content value="inbound" class="select-none pt-3">Test2</Tabs.Content>
|
||||
<Tabs.Content
|
||||
value="login"
|
||||
class="bg-mantle border-surface-0 mt-2 rounded-lg border p-6 drop-shadow-md"
|
||||
>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const username = formData.get('login-username')?.toString();
|
||||
const password = formData.get('login-password')?.toString();
|
||||
|
||||
try {
|
||||
const response = await AuthClient.login({
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (response.token && username) {
|
||||
localStorage.setItem('username', username);
|
||||
goto('/');
|
||||
}
|
||||
} catch (err) {
|
||||
const error = ConnectError.from(err);
|
||||
toast.error(error.rawMessage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="login-username" class="text-sm">Username</label>
|
||||
<input
|
||||
id="login-username"
|
||||
name="login-username"
|
||||
type="text"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="login-password" class="text-sm">Password</label>
|
||||
<input
|
||||
id="login-password"
|
||||
name="login-password"
|
||||
type="password"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Button.Root
|
||||
type="submit"
|
||||
class="bg-sky text-crust hover:brightness-120 w-20 cursor-pointer rounded p-2 px-4 text-sm transition-all"
|
||||
>
|
||||
Submit
|
||||
</Button.Root>
|
||||
</div>
|
||||
</form>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
value="signup"
|
||||
class="bg-mantle border-surface-0 mt-2 rounded-lg border p-6 drop-shadow-md"
|
||||
>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
await AuthClient.signUp({
|
||||
username: formData.get('signup-username')?.toString(),
|
||||
password: formData.get('signup-password')?.toString(),
|
||||
confirmPassword: formData.get('signup-confirm-password')?.toString()
|
||||
});
|
||||
|
||||
toast.success('account created successfully, please log in');
|
||||
form.reset();
|
||||
tab = 'login';
|
||||
} catch (err) {
|
||||
const error = ConnectError.from(err);
|
||||
toast.error(error.rawMessage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="signup-username" class="text-sm">Username</label>
|
||||
<input
|
||||
id="signup-username"
|
||||
name="signup-username"
|
||||
type="text"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="signup-password" class="text-sm">Password</label>
|
||||
<input
|
||||
id="signup-password"
|
||||
name="signup-password"
|
||||
type="password"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="signup-confirm-password" class="text-sm">Confirm Password</label>
|
||||
<input
|
||||
id="signup-confirm-password"
|
||||
name="signup-confirm-password"
|
||||
type="password"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Button.Root
|
||||
type="submit"
|
||||
class="bg-sky text-crust hover:brightness-120 w-20 cursor-pointer rounded p-2 px-4 text-sm transition-all"
|
||||
>
|
||||
Submit
|
||||
</Button.Root>
|
||||
</div>
|
||||
</form>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
|
@ -51,6 +51,9 @@ components:
|
||||
password:
|
||||
type: string
|
||||
title: password
|
||||
confirmPassword:
|
||||
type: string
|
||||
title: confirm_password
|
||||
title: SignUpRequest
|
||||
additionalProperties: false
|
||||
user.v1.SignUpResponse:
|
||||
|
24
flake.nix
24
flake.nix
@ -38,7 +38,7 @@
|
||||
pname = "${pname}-client";
|
||||
inherit version;
|
||||
src = gitignore.lib.gitignoreSource ./client;
|
||||
npmDepsHash = "sha256-hOmZZrCSuHyRQhG6M7Yu5uRLTdCYOL/giT4zUm9iTRE=";
|
||||
npmDepsHash = "sha256-YCmx+XNir+6iI0zqIBIjlNxyQ5iz4j9Is+unfSMEgQE=";
|
||||
nodejs = pkgs.nodejs_22;
|
||||
|
||||
installPhase = ''
|
||||
@ -68,6 +68,9 @@
|
||||
# Svelte frontend
|
||||
nodejs_22
|
||||
|
||||
# Openapi gen
|
||||
openapi-generator-cli
|
||||
|
||||
# Helper scripts
|
||||
(writeShellApplication {
|
||||
name = "run";
|
||||
@ -91,25 +94,6 @@
|
||||
'';
|
||||
})
|
||||
|
||||
(writeShellApplication {
|
||||
name = "build";
|
||||
|
||||
text = ''
|
||||
gitroot=$(git rev-parse --show-toplevel)
|
||||
|
||||
cd "''${gitroot}"
|
||||
buf lint
|
||||
buf generate
|
||||
|
||||
cd "''${gitroot}/client"
|
||||
npm run build
|
||||
cp -r build ../server/client
|
||||
|
||||
cd "''${gitroot}/server"
|
||||
go build -o ../build/trevstack .
|
||||
'';
|
||||
})
|
||||
|
||||
(writeShellApplication {
|
||||
name = "protobufwatch";
|
||||
|
||||
|
@ -19,6 +19,7 @@ message LoginResponse {
|
||||
message SignUpRequest {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
string confirm_password = 3;
|
||||
}
|
||||
message SignUpResponse {}
|
||||
|
||||
|
@ -77,6 +77,9 @@ func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si
|
||||
} else {
|
||||
return nil, connect.NewError(connect.CodeAlreadyExists, errors.New("username already exists"))
|
||||
}
|
||||
if req.Msg.Password != req.Msg.ConfirmPassword {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Msg.Password), bcrypt.DefaultCost)
|
||||
|
@ -7,11 +7,51 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func WithAuthRedirect(next http.Handler, key string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("start", "method", r.Method, "path", r.URL.Path)
|
||||
pathItems := strings.Split(r.URL.Path, "/")
|
||||
|
||||
if len(pathItems) < 2 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch pathItems[1] {
|
||||
case "auth":
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
|
||||
case "_app":
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
|
||||
default:
|
||||
// Check if the request contains a valid cookie token
|
||||
cookies := getCookies(r.Header.Get("Cookie"))
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "token" {
|
||||
_, err := validateToken(cookie.Value, key)
|
||||
if err == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise redirect
|
||||
http.Redirect(w, r, "/auth", http.StatusFound)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type authInterceptor struct {
|
||||
key string
|
||||
}
|
||||
|
@ -118,11 +118,12 @@ func (x *LoginResponse) GetToken() string {
|
||||
}
|
||||
|
||||
type SignUpRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||
ConfirmPassword string `protobuf:"bytes,3,opt,name=confirm_password,json=confirmPassword,proto3" json:"confirm_password,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SignUpRequest) Reset() {
|
||||
@ -169,6 +170,13 @@ func (x *SignUpRequest) GetPassword() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SignUpRequest) GetConfirmPassword() string {
|
||||
if x != nil {
|
||||
return x.ConfirmPassword
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SignUpResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
@ -288,38 +296,41 @@ var file_user_v1_auth_proto_rawDesc = string([]byte{
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x47, 0x0a, 0x0d,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x72, 0x0a, 0x0d,
|
||||
0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
|
||||
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x6f, 0x75,
|
||||
0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x10, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f,
|
||||
0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc1, 0x01, 0x0a, 0x0b, 0x41,
|
||||
0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x4c, 0x6f,
|
||||
0x67, 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f,
|
||||
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x75, 0x73, 0x65,
|
||||
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x12, 0x16,
|
||||
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
||||
0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x2e, 0x75, 0x73,
|
||||
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f,
|
||||
0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x9d,
|
||||
0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09,
|
||||
0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, 0x69, 0x74,
|
||||
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x6f, 0x74, 0x64, 0x65, 0x6d, 0x6f,
|
||||
0x34, 0x2f, 0x74, 0x72, 0x65, 0x76, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x73, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65,
|
||||
0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72,
|
||||
0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13,
|
||||
0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d,
|
||||
0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
|
||||
0x22, 0x10, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x22, 0x10, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc1, 0x01, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65,
|
||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x15,
|
||||
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
|
||||
0x3b, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x12, 0x16, 0x2e, 0x75, 0x73, 0x65, 0x72,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x1a, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e,
|
||||
0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06,
|
||||
0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x16, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17,
|
||||
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x9d, 0x01, 0x0a, 0x0b, 0x63, 0x6f,
|
||||
0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x41, 0x75, 0x74, 0x68, 0x50,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
|
||||
0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x6f, 0x74, 0x64, 0x65, 0x6d, 0x6f, 0x34, 0x2f, 0x74, 0x72, 0x65,
|
||||
0x76, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x69, 0x6e,
|
||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f,
|
||||
0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02,
|
||||
0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02,
|
||||
0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x5c,
|
||||
0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02,
|
||||
0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/spotdemo4/trevstack/server/internal/database"
|
||||
"github.com/spotdemo4/trevstack/server/internal/handlers"
|
||||
"github.com/spotdemo4/trevstack/server/internal/interceptors"
|
||||
)
|
||||
|
||||
//go:embed all:client
|
||||
@ -120,7 +121,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get sub filesystem: %v", err)
|
||||
}
|
||||
mux.Handle("/", http.FileServer(http.FS(clientFs)))
|
||||
mux.Handle("/", interceptors.WithAuthRedirect(http.FileServer(http.FS(clientFs)), env.Key))
|
||||
mux.Handle("/grpc/", http.StripPrefix("/grpc", api))
|
||||
|
||||
// Start server
|
||||
|
Loading…
x
Reference in New Issue
Block a user