mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
Add auth demo
This commit is contained in:
9
src/Website/.react-router/types/+future.ts
Normal file
9
src/Website/.react-router/types/+future.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import "react-router";
|
||||||
|
|
||||||
|
declare module "react-router" {
|
||||||
|
interface Future {
|
||||||
|
v8_middleware: false
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/Website/.react-router/types/+routes.ts
Normal file
97
src/Website/.react-router/types/+routes.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import "react-router"
|
||||||
|
|
||||||
|
declare module "react-router" {
|
||||||
|
interface Register {
|
||||||
|
pages: Pages
|
||||||
|
routeFiles: RouteFiles
|
||||||
|
routeModules: RouteModules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pages = {
|
||||||
|
"/": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/login": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/register": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/logout": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/dashboard": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/confirm": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/beers": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/breweries": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
"/beer-styles": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteFiles = {
|
||||||
|
"root.tsx": {
|
||||||
|
id: "root";
|
||||||
|
page: "/" | "/login" | "/register" | "/logout" | "/dashboard" | "/confirm" | "/beers" | "/breweries" | "/beer-styles";
|
||||||
|
};
|
||||||
|
"routes/home.tsx": {
|
||||||
|
id: "routes/home";
|
||||||
|
page: "/";
|
||||||
|
};
|
||||||
|
"routes/login.tsx": {
|
||||||
|
id: "routes/login";
|
||||||
|
page: "/login";
|
||||||
|
};
|
||||||
|
"routes/register.tsx": {
|
||||||
|
id: "routes/register";
|
||||||
|
page: "/register";
|
||||||
|
};
|
||||||
|
"routes/logout.tsx": {
|
||||||
|
id: "routes/logout";
|
||||||
|
page: "/logout";
|
||||||
|
};
|
||||||
|
"routes/dashboard.tsx": {
|
||||||
|
id: "routes/dashboard";
|
||||||
|
page: "/dashboard";
|
||||||
|
};
|
||||||
|
"routes/confirm.tsx": {
|
||||||
|
id: "routes/confirm";
|
||||||
|
page: "/confirm";
|
||||||
|
};
|
||||||
|
"routes/beers.tsx": {
|
||||||
|
id: "routes/beers";
|
||||||
|
page: "/beers";
|
||||||
|
};
|
||||||
|
"routes/breweries.tsx": {
|
||||||
|
id: "routes/breweries";
|
||||||
|
page: "/breweries";
|
||||||
|
};
|
||||||
|
"routes/beer-styles.tsx": {
|
||||||
|
id: "routes/beer-styles";
|
||||||
|
page: "/beer-styles";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteModules = {
|
||||||
|
"root": typeof import("./app/root.tsx");
|
||||||
|
"routes/home": typeof import("./app/routes/home.tsx");
|
||||||
|
"routes/login": typeof import("./app/routes/login.tsx");
|
||||||
|
"routes/register": typeof import("./app/routes/register.tsx");
|
||||||
|
"routes/logout": typeof import("./app/routes/logout.tsx");
|
||||||
|
"routes/dashboard": typeof import("./app/routes/dashboard.tsx");
|
||||||
|
"routes/confirm": typeof import("./app/routes/confirm.tsx");
|
||||||
|
"routes/beers": typeof import("./app/routes/beers.tsx");
|
||||||
|
"routes/breweries": typeof import("./app/routes/breweries.tsx");
|
||||||
|
"routes/beer-styles": typeof import("./app/routes/beer-styles.tsx");
|
||||||
|
};
|
||||||
18
src/Website/.react-router/types/+server-build.d.ts
vendored
Normal file
18
src/Website/.react-router/types/+server-build.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
declare module "virtual:react-router/server-build" {
|
||||||
|
import { ServerBuild } from "react-router";
|
||||||
|
export const assets: ServerBuild["assets"];
|
||||||
|
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
|
||||||
|
export const basename: ServerBuild["basename"];
|
||||||
|
export const entry: ServerBuild["entry"];
|
||||||
|
export const future: ServerBuild["future"];
|
||||||
|
export const isSpaMode: ServerBuild["isSpaMode"];
|
||||||
|
export const prerender: ServerBuild["prerender"];
|
||||||
|
export const publicPath: ServerBuild["publicPath"];
|
||||||
|
export const routeDiscovery: ServerBuild["routeDiscovery"];
|
||||||
|
export const routes: ServerBuild["routes"];
|
||||||
|
export const ssr: ServerBuild["ssr"];
|
||||||
|
export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
|
||||||
|
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
|
||||||
|
}
|
||||||
59
src/Website/.react-router/types/app/+types/root.ts
Normal file
59
src/Website/.react-router/types/app/+types/root.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../root.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "root.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../root.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../beer-styles.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/beer-styles.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/beer-styles";
|
||||||
|
module: typeof import("../beer-styles.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
62
src/Website/.react-router/types/app/routes/+types/beers.ts
Normal file
62
src/Website/.react-router/types/app/routes/+types/beers.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../beers.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/beers.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/beers";
|
||||||
|
module: typeof import("../beers.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../breweries.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/breweries.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/breweries";
|
||||||
|
module: typeof import("../breweries.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
62
src/Website/.react-router/types/app/routes/+types/confirm.ts
Normal file
62
src/Website/.react-router/types/app/routes/+types/confirm.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../confirm.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/confirm.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/confirm";
|
||||||
|
module: typeof import("../confirm.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../dashboard.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/dashboard.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/dashboard";
|
||||||
|
module: typeof import("../dashboard.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
62
src/Website/.react-router/types/app/routes/+types/home.ts
Normal file
62
src/Website/.react-router/types/app/routes/+types/home.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../home.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/home.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/home";
|
||||||
|
module: typeof import("../home.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
62
src/Website/.react-router/types/app/routes/+types/login.ts
Normal file
62
src/Website/.react-router/types/app/routes/+types/login.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../login.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/login.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/login";
|
||||||
|
module: typeof import("../login.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
62
src/Website/.react-router/types/app/routes/+types/logout.ts
Normal file
62
src/Website/.react-router/types/app/routes/+types/logout.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../logout.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/logout.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/logout";
|
||||||
|
module: typeof import("../logout.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../register.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/register.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/register";
|
||||||
|
module: typeof import("../register.js");
|
||||||
|
}];
|
||||||
|
|
||||||
|
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||||
|
|
||||||
|
export namespace Route {
|
||||||
|
// links
|
||||||
|
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||||
|
export type LinksFunction = Annotations["LinksFunction"];
|
||||||
|
|
||||||
|
// meta
|
||||||
|
export type MetaArgs = Annotations["MetaArgs"];
|
||||||
|
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||||
|
export type MetaFunction = Annotations["MetaFunction"];
|
||||||
|
|
||||||
|
// headers
|
||||||
|
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||||
|
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||||
|
|
||||||
|
// clientMiddleware
|
||||||
|
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||||
|
|
||||||
|
// loader
|
||||||
|
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||||
|
|
||||||
|
// clientLoader
|
||||||
|
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||||
|
|
||||||
|
// action
|
||||||
|
export type ActionArgs = Annotations["ActionArgs"];
|
||||||
|
|
||||||
|
// clientAction
|
||||||
|
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||||
|
|
||||||
|
// HydrateFallback
|
||||||
|
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||||
|
|
||||||
|
// Component
|
||||||
|
export type ComponentProps = Annotations["ComponentProps"];
|
||||||
|
|
||||||
|
// ErrorBoundary
|
||||||
|
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||||
|
}
|
||||||
8
src/Website/.vite/deps/_metadata.json
Normal file
8
src/Website/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "fdf55a9c",
|
||||||
|
"configHash": "c6a852b3",
|
||||||
|
"lockfileHash": "e3b0c442",
|
||||||
|
"browserHash": "58d74d30",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
3
src/Website/.vite/deps/package.json
Normal file
3
src/Website/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
118
src/Website/app/app.css
Normal file
118
src/Website/app/app.css
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: biergarten-lager, biergarten-stout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans:
|
||||||
|
"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--font-serif: "Volkhov", ui-serif, Georgia, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
.card-title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "biergarten-lager";
|
||||||
|
default: true;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
/* Base — warm parchment / aged paper */
|
||||||
|
--color-base-100: oklch(97% 0.025 80);
|
||||||
|
--color-base-200: oklch(92% 0.04 78);
|
||||||
|
--color-base-300: oklch(86% 0.06 75);
|
||||||
|
--color-base-content: oklch(28% 0.05 55);
|
||||||
|
/* Primary — golden amber lager */
|
||||||
|
--color-primary: oklch(68% 0.165 60);
|
||||||
|
--color-primary-content: oklch(18% 0.04 55);
|
||||||
|
/* Secondary — deep mahogany ale */
|
||||||
|
--color-secondary: oklch(38% 0.09 40);
|
||||||
|
--color-secondary-content: oklch(95% 0.02 75);
|
||||||
|
/* Accent — frothy cream head */
|
||||||
|
--color-accent: oklch(94% 0.03 90);
|
||||||
|
--color-accent-content: oklch(30% 0.05 55);
|
||||||
|
/* Neutral — roasted stout */
|
||||||
|
--color-neutral: oklch(24% 0.04 45);
|
||||||
|
--color-neutral-content: oklch(92% 0.025 80);
|
||||||
|
/* Info — cool hop green */
|
||||||
|
--color-info: oklch(58% 0.14 145);
|
||||||
|
--color-info-content: oklch(97% 0.015 145);
|
||||||
|
/* Success — fresh barley */
|
||||||
|
--color-success: oklch(72% 0.13 120);
|
||||||
|
--color-success-content: oklch(20% 0.04 120);
|
||||||
|
/* Warning — amber harvest */
|
||||||
|
--color-warning: oklch(74% 0.19 55);
|
||||||
|
--color-warning-content: oklch(18% 0.04 55);
|
||||||
|
/* Error — deep cherry kriek */
|
||||||
|
--color-error: oklch(52% 0.2 20);
|
||||||
|
--color-error-content: oklch(97% 0.012 15);
|
||||||
|
--radius-selector: 0.375rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 0.875rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 1;
|
||||||
|
}
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "biergarten-stout";
|
||||||
|
default: false;
|
||||||
|
prefersdark: true;
|
||||||
|
color-scheme: "dark";
|
||||||
|
|
||||||
|
/* Base — charred barrel / roasted malt darkness */
|
||||||
|
--color-base-100: oklch(14% 0.006 45);
|
||||||
|
--color-base-200: oklch(18% 0.008 43);
|
||||||
|
--color-base-300: oklch(23% 0.01 42);
|
||||||
|
--color-base-content: oklch(88% 0.008 75);
|
||||||
|
|
||||||
|
/* Primary — golden amber lager */
|
||||||
|
--color-primary: oklch(68% 0.055 60);
|
||||||
|
--color-primary-content: oklch(14% 0.012 50);
|
||||||
|
|
||||||
|
/* Secondary — deep mahogany ale */
|
||||||
|
--color-secondary: oklch(55% 0.025 40);
|
||||||
|
--color-secondary-content: oklch(97% 0.005 75);
|
||||||
|
|
||||||
|
/* Accent — frothy cream head */
|
||||||
|
--color-accent: oklch(82% 0.01 88);
|
||||||
|
--color-accent-content: oklch(20% 0.01 55);
|
||||||
|
|
||||||
|
/* Neutral — near-black with warmth */
|
||||||
|
--color-neutral: oklch(20% 0.008 45);
|
||||||
|
--color-neutral-content: oklch(88% 0.007 78);
|
||||||
|
|
||||||
|
/* Info — cool hop green */
|
||||||
|
--color-info: oklch(54% 0.04 145);
|
||||||
|
--color-info-content: oklch(97% 0.005 145);
|
||||||
|
|
||||||
|
/* Success — fresh barley */
|
||||||
|
--color-success: oklch(66% 0.038 120);
|
||||||
|
--color-success-content: oklch(14% 0.012 120);
|
||||||
|
|
||||||
|
/* Warning — amber harvest */
|
||||||
|
--color-warning: oklch(70% 0.055 55);
|
||||||
|
--color-warning-content: oklch(14% 0.012 55);
|
||||||
|
|
||||||
|
/* Error — deep cherry kriek */
|
||||||
|
--color-error: oklch(50% 0.06 20);
|
||||||
|
--color-error-content: oklch(97% 0.004 15);
|
||||||
|
|
||||||
|
--radius-selector: 0.375rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 0.875rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 1;
|
||||||
|
}
|
||||||
59
src/Website/app/components/Navbar.tsx
Normal file
59
src/Website/app/components/Navbar.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
auth: {
|
||||||
|
username: string;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
userAccountId: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Navbar({ auth }: NavbarProps) {
|
||||||
|
return (
|
||||||
|
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50">
|
||||||
|
<div className="navbar-start">
|
||||||
|
<Link to="/" className="text-xl font-bold px-4">
|
||||||
|
🍺 The Biergarten App
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="navbar-center gap-4">
|
||||||
|
<Link to="/beers" className="btn btn-ghost btn-sm">
|
||||||
|
Beers
|
||||||
|
</Link>
|
||||||
|
<Link to="/breweries" className="btn btn-ghost btn-sm">
|
||||||
|
Breweries
|
||||||
|
</Link>
|
||||||
|
<Link to="/beer-styles" className="btn btn-ghost btn-sm">
|
||||||
|
Beer Styles
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="navbar-end gap-2 pr-4">
|
||||||
|
<Link to="/register" className="btn btn-ghost btn-sm">
|
||||||
|
Register User
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{auth ? (
|
||||||
|
<>
|
||||||
|
<Link to="/dashboard" className="btn btn-primary btn-sm">
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<div className="divider divider-horizontal m-0 h-6"></div>
|
||||||
|
<span className="text-sm text-base-content/70">
|
||||||
|
{auth.username}
|
||||||
|
</span>
|
||||||
|
<Link to="/logout" className="btn btn-ghost btn-sm">
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link to="/login" className="btn btn-primary btn-sm">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
src/Website/app/lib/auth.server.ts
Normal file
175
src/Website/app/lib/auth.server.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { createCookieSessionStorage, redirect } from "react-router";
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:8080";
|
||||||
|
|
||||||
|
export interface AuthTokens {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
userAccountId: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
message: string;
|
||||||
|
payload: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginPayload {
|
||||||
|
userAccountId: string;
|
||||||
|
username: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegistrationPayload extends LoginPayload {
|
||||||
|
confirmationEmailSent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionStorage = createCookieSessionStorage({
|
||||||
|
cookie: {
|
||||||
|
name: "__session",
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 60 * 60 * 24 * 21, // 21 days (matches refresh token)
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
secrets: [process.env.SESSION_SECRET || "dev-secret-change-me"],
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getSession(request: Request) {
|
||||||
|
return sessionStorage.getSession(request.headers.get("Cookie"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function commitSession(
|
||||||
|
session: Awaited<ReturnType<typeof getSession>>,
|
||||||
|
) {
|
||||||
|
return sessionStorage.commitSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function destroySession(
|
||||||
|
session: Awaited<ReturnType<typeof getSession>>,
|
||||||
|
) {
|
||||||
|
return sessionStorage.destroySession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireAuth(request: Request): Promise<AuthTokens> {
|
||||||
|
const session = await getSession(request);
|
||||||
|
const accessToken = session.get("accessToken");
|
||||||
|
const refreshToken = session.get("refreshToken");
|
||||||
|
|
||||||
|
if (!accessToken || !refreshToken) {
|
||||||
|
throw redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
userAccountId: session.get("userAccountId"),
|
||||||
|
username: session.get("username"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOptionalAuth(
|
||||||
|
request: Request,
|
||||||
|
): Promise<AuthTokens | null> {
|
||||||
|
const session = await getSession(request);
|
||||||
|
const accessToken = session.get("accessToken");
|
||||||
|
|
||||||
|
if (!accessToken) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: session.get("refreshToken"),
|
||||||
|
userAccountId: session.get("userAccountId"),
|
||||||
|
username: session.get("username"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(username: string, password: string) {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(text || `Login failed (${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<LoginPayload> = await res.json();
|
||||||
|
return data.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function register(body: {
|
||||||
|
username: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
dateOfBirth: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(text || `Registration failed (${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<RegistrationPayload> = await res.json();
|
||||||
|
return data.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshTokens(refreshToken: string) {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ refreshToken }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Token refresh failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<LoginPayload> = await res.json();
|
||||||
|
return data.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmEmail(token: string, accessToken: string) {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE_URL}/api/auth/confirm?token=${encodeURIComponent(token)}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(text || `Confirmation failed (${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ApiResponse<{ userAccountId: string; confirmedDate: string }> =
|
||||||
|
await res.json();
|
||||||
|
return data.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAuthSession(
|
||||||
|
payload: LoginPayload,
|
||||||
|
redirectTo: string,
|
||||||
|
) {
|
||||||
|
const session = await sessionStorage.getSession();
|
||||||
|
session.set("accessToken", payload.accessToken);
|
||||||
|
session.set("refreshToken", payload.refreshToken);
|
||||||
|
session.set("userAccountId", payload.userAccountId);
|
||||||
|
session.set("username", payload.username);
|
||||||
|
|
||||||
|
return redirect(redirectTo, {
|
||||||
|
headers: { "Set-Cookie": await commitSession(session) },
|
||||||
|
});
|
||||||
|
}
|
||||||
33
src/Website/app/lib/schemas.ts
Normal file
33
src/Website/app/lib/schemas.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const loginSchema = z.object({
|
||||||
|
username: z.string().min(1, "Username is required"),
|
||||||
|
password: z.string().min(1, "Password is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginSchema = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
|
export const registerSchema = z
|
||||||
|
.object({
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(3, "Username must be at least 3 characters")
|
||||||
|
.max(20, "Username must be at most 20 characters"),
|
||||||
|
firstName: z.string().min(1, "First name is required"),
|
||||||
|
lastName: z.string().min(1, "Last name is required"),
|
||||||
|
email: z.string().email("Invalid email address"),
|
||||||
|
dateOfBirth: z.string().min(1, "Date of birth is required"),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, "Password must be at least 8 characters")
|
||||||
|
.regex(/[A-Z]/, "Password must contain an uppercase letter")
|
||||||
|
.regex(/[a-z]/, "Password must contain a lowercase letter")
|
||||||
|
.regex(/[0-9]/, "Password must contain a number"),
|
||||||
|
confirmPassword: z.string().min(1, "Please confirm your password"),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: "Passwords must match",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RegisterSchema = z.infer<typeof registerSchema>;
|
||||||
88
src/Website/app/root.tsx
Normal file
88
src/Website/app/root.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
isRouteErrorResponse,
|
||||||
|
Links,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
} from "react-router";
|
||||||
|
|
||||||
|
import type { Route } from "./+types/root";
|
||||||
|
import "./app.css";
|
||||||
|
import Navbar from "./components/Navbar";
|
||||||
|
import { getOptionalAuth } from "./lib/auth.server";
|
||||||
|
|
||||||
|
export const links: Route.LinksFunction = () => [
|
||||||
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
|
{
|
||||||
|
rel: "preconnect",
|
||||||
|
href: "https://fonts.gstatic.com",
|
||||||
|
crossOrigin: "anonymous",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..900;1,9..40,100..900&family=Volkhov:ital,wght@0,400;0,700;1,400;1,700&display=swap",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||||
|
const auth = await getOptionalAuth(request);
|
||||||
|
return { auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { auth } = loaderData;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar auth={auth} />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
let message = "Oops!";
|
||||||
|
let details = "An unexpected error occurred.";
|
||||||
|
let stack: string | undefined;
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
message = error.status === 404 ? "404" : "Error";
|
||||||
|
details =
|
||||||
|
error.status === 404
|
||||||
|
? "The requested page could not be found."
|
||||||
|
: error.statusText || details;
|
||||||
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||||
|
details = error.message;
|
||||||
|
stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="pt-16 p-4 container mx-auto">
|
||||||
|
<h1>{message}</h1>
|
||||||
|
<p>{details}</p>
|
||||||
|
{stack && (
|
||||||
|
<pre className="w-full p-4 overflow-x-auto">
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/Website/app/routes.ts
Normal file
13
src/Website/app/routes.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
index("routes/home.tsx"),
|
||||||
|
route("login", "routes/login.tsx"),
|
||||||
|
route("register", "routes/register.tsx"),
|
||||||
|
route("logout", "routes/logout.tsx"),
|
||||||
|
route("dashboard", "routes/dashboard.tsx"),
|
||||||
|
route("confirm", "routes/confirm.tsx"),
|
||||||
|
route("beers", "routes/beers.tsx"),
|
||||||
|
route("breweries", "routes/breweries.tsx"),
|
||||||
|
route("beer-styles", "routes/beer-styles.tsx"),
|
||||||
|
] satisfies RouteConfig;
|
||||||
18
src/Website/app/routes/beer-styles.tsx
Normal file
18
src/Website/app/routes/beer-styles.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Route } from "./+types/beer-styles";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Beer Styles | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BeerStyles() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-base-200">
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Beer Styles</h1>
|
||||||
|
<p className="text-base-content/70">
|
||||||
|
Learn about different beer styles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/Website/app/routes/beers.tsx
Normal file
16
src/Website/app/routes/beers.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Route } from "./+types/beers";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Beers | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Beers() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-base-200">
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Beers</h1>
|
||||||
|
<p className="text-base-content/70">Explore our collection of beers.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/Website/app/routes/breweries.tsx
Normal file
16
src/Website/app/routes/breweries.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Route } from "./+types/breweries";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Breweries | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Breweries() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-base-200">
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Breweries</h1>
|
||||||
|
<p className="text-base-content/70">Discover our partner breweries.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/Website/app/routes/confirm.tsx
Normal file
80
src/Website/app/routes/confirm.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Link } from "react-router";
|
||||||
|
import { confirmEmail, requireAuth } from "../lib/auth.server";
|
||||||
|
import type { Route } from "./+types/confirm";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Confirm Email | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const auth = await requireAuth(request);
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const token = url.searchParams.get("token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return { success: false as const, error: "Missing confirmation token." };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await confirmEmail(token, auth.accessToken);
|
||||||
|
return {
|
||||||
|
success: true as const,
|
||||||
|
confirmedDate: payload.confirmedDate,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
success: false as const,
|
||||||
|
error: err instanceof Error ? err.message : "Confirmation failed.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Confirm({ loaderData }: Route.ComponentProps) {
|
||||||
|
return (
|
||||||
|
<div className="hero min-h-screen bg-base-200">
|
||||||
|
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body items-center text-center gap-4">
|
||||||
|
{loaderData.success ? (
|
||||||
|
<>
|
||||||
|
<div className="text-success text-6xl">✓</div>
|
||||||
|
<h1 className="card-title text-2xl">Email Confirmed!</h1>
|
||||||
|
<p className="text-base-content/70">
|
||||||
|
Your email address has been successfully verified.
|
||||||
|
</p>
|
||||||
|
<div className="bg-base-200 rounded-box w-full p-3 text-sm text-left">
|
||||||
|
<span className="text-base-content/50 text-xs uppercase tracking-widest font-semibold">
|
||||||
|
Confirmed at
|
||||||
|
</span>
|
||||||
|
<p className="font-mono mt-1">
|
||||||
|
{new Date(loaderData.confirmedDate).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions w-full pt-2">
|
||||||
|
<Link to="/dashboard" className="btn btn-primary w-full">
|
||||||
|
Go to Dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-error text-6xl">✕</div>
|
||||||
|
<h1 className="card-title text-2xl">Confirmation Failed</h1>
|
||||||
|
<div role="alert" className="alert alert-error alert-soft w-full">
|
||||||
|
<span>{loaderData.error}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-base-content/70 text-sm">
|
||||||
|
The confirmation link may have expired (valid for 30 minutes) or
|
||||||
|
already been used.
|
||||||
|
</p>
|
||||||
|
<div className="card-actions w-full pt-2 flex-col gap-2">
|
||||||
|
<Link to="/dashboard" className="btn btn-primary w-full">
|
||||||
|
Back to Dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/Website/app/routes/dashboard.tsx
Normal file
110
src/Website/app/routes/dashboard.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { requireAuth } from "../lib/auth.server";
|
||||||
|
import type { Route } from "./+types/dashboard";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Dashboard | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const auth = await requireAuth(request);
|
||||||
|
return {
|
||||||
|
username: auth.username,
|
||||||
|
userAccountId: auth.userAccountId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Dashboard({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { username, userAccountId } = loaderData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-base-200">
|
||||||
|
<div className="mx-auto max-w-4xl px-6 py-10 space-y-6">
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-2xl">Welcome, {username}!</h2>
|
||||||
|
<p className="text-base-content/70">
|
||||||
|
You are successfully authenticated. This is a protected page that
|
||||||
|
requires a valid session.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-base-200 rounded-box p-4 mt-2">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-widest text-base-content/50 mb-3">
|
||||||
|
Session Info
|
||||||
|
</p>
|
||||||
|
<div className="stats stats-vertical w-full">
|
||||||
|
<div className="stat py-2">
|
||||||
|
<div className="stat-title">Username</div>
|
||||||
|
<div className="stat-value text-lg font-mono">{username}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat py-2">
|
||||||
|
<div className="stat-title">User ID</div>
|
||||||
|
<div className="stat-desc font-mono text-xs mt-1">
|
||||||
|
{userAccountId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-100 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">Auth Flow Demo</h2>
|
||||||
|
<p className="text-sm text-base-content/70">
|
||||||
|
This demo showcases the following authentication features:
|
||||||
|
</p>
|
||||||
|
<ul className="list">
|
||||||
|
<li className="list-row">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Login</p>
|
||||||
|
<p className="text-sm text-base-content/60">
|
||||||
|
POST to <code className="kbd kbd-sm">/api/auth/login</code>{" "}
|
||||||
|
with username & password
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="list-row">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Register</p>
|
||||||
|
<p className="text-sm text-base-content/60">
|
||||||
|
POST to{" "}
|
||||||
|
<code className="kbd kbd-sm">/api/auth/register</code> with
|
||||||
|
full user details
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="list-row">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Session</p>
|
||||||
|
<p className="text-sm text-base-content/60">
|
||||||
|
JWT access & refresh tokens stored in an HTTP-only
|
||||||
|
cookie
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="list-row">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Protected Routes</p>
|
||||||
|
<p className="text-sm text-base-content/60">
|
||||||
|
This dashboard requires authentication via{" "}
|
||||||
|
<code className="kbd kbd-sm">requireAuth()</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="list-row">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Token Refresh</p>
|
||||||
|
<p className="text-sm text-base-content/60">
|
||||||
|
POST to{" "}
|
||||||
|
<code className="kbd kbd-sm">/api/auth/refresh</code> with
|
||||||
|
refresh token
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/Website/app/routes/home.tsx
Normal file
56
src/Website/app/routes/home.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Link } from "react-router";
|
||||||
|
import { getOptionalAuth } from "../lib/auth.server";
|
||||||
|
import type { Route } from "./+types/home";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "The Biergarten App" },
|
||||||
|
{ name: "description", content: "Welcome to The Biergarten App" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const auth = await getOptionalAuth(request);
|
||||||
|
return { username: auth?.username ?? null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({ loaderData }: Route.ComponentProps) {
|
||||||
|
const { username } = loaderData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hero min-h-screen bg-base-200">
|
||||||
|
<div className="hero-content text-center">
|
||||||
|
<div className="max-w-md space-y-6">
|
||||||
|
<h1 className="text-5xl font-bold">🍺 The Biergarten App</h1>
|
||||||
|
<p className="text-lg text-base-content/70">Authentication Demo</p>
|
||||||
|
|
||||||
|
{username ? (
|
||||||
|
<>
|
||||||
|
<p className="text-base-content/80">
|
||||||
|
Welcome back,{" "}
|
||||||
|
<span className="font-semibold text-primary">{username}</span>!
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3 justify-center">
|
||||||
|
<Link to="/dashboard" className="btn btn-primary">
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<Link to="/logout" className="btn btn-ghost">
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-3 justify-center">
|
||||||
|
<Link to="/login" className="btn btn-primary">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
<Link to="/register" className="btn btn-outline">
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
src/Website/app/routes/login.tsx
Normal file
133
src/Website/app/routes/login.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||||
|
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
|
||||||
|
import { loginSchema, type LoginSchema } from "../lib/schemas";
|
||||||
|
import type { Route } from "./+types/login";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Login | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const auth = await getOptionalAuth(request);
|
||||||
|
if (auth) throw redirect("/dashboard");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function action({ request }: Route.ActionArgs) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const result = loginSchema.safeParse({
|
||||||
|
username: formData.get("username"),
|
||||||
|
password: formData.get("password"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return { error: result.error.issues[0].message };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await login(result.data.username, result.data.password);
|
||||||
|
return createAuthSession(payload, "/dashboard");
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err instanceof Error ? err.message : "Login failed." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Login({ actionData }: Route.ComponentProps) {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const submit = useSubmit();
|
||||||
|
const isSubmitting = navigation.state === "submitting";
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<LoginSchema>({ resolver: zodResolver(loginSchema) });
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
submit(data, { method: "post" });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hero min-h-screen bg-base-200">
|
||||||
|
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="card-title text-3xl justify-center">Login</h1>
|
||||||
|
<p className="text-base-content/70">
|
||||||
|
Sign in to your Biergarten account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actionData?.error && (
|
||||||
|
<div role="alert" className="alert alert-error alert-soft">
|
||||||
|
<span>{actionData.error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit} className="space-y-3">
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Username</legend>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
placeholder="your_username"
|
||||||
|
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
||||||
|
{...register("username")}
|
||||||
|
/>
|
||||||
|
{errors.username && (
|
||||||
|
<p className="label text-error">{errors.username.message}</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Password</legend>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
||||||
|
{...register("password")}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="label text-error">{errors.password.message}</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="btn btn-primary w-full mt-2"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<span className="loading loading-spinner loading-sm" />{" "}
|
||||||
|
Signing in...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Sign In"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="divider text-xs">New here?</div>
|
||||||
|
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<Link to="/register" className="btn btn-outline btn-sm w-full">
|
||||||
|
Create an account
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="link link-hover text-sm text-base-content/60"
|
||||||
|
>
|
||||||
|
← Back to home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/Website/app/routes/logout.tsx
Normal file
10
src/Website/app/routes/logout.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { redirect } from "react-router";
|
||||||
|
import { destroySession, getSession } from "../lib/auth.server";
|
||||||
|
import type { Route } from "./+types/logout";
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const session = await getSession(request);
|
||||||
|
return redirect("/", {
|
||||||
|
headers: { "Set-Cookie": await destroySession(session) },
|
||||||
|
});
|
||||||
|
}
|
||||||
231
src/Website/app/routes/register.tsx
Normal file
231
src/Website/app/routes/register.tsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||||
|
import {
|
||||||
|
createAuthSession,
|
||||||
|
getOptionalAuth,
|
||||||
|
register,
|
||||||
|
} from "../lib/auth.server";
|
||||||
|
import { registerSchema, type RegisterSchema } from "../lib/schemas";
|
||||||
|
import type { Route } from "./+types/register";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [{ title: "Register | The Biergarten App" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const auth = await getOptionalAuth(request);
|
||||||
|
if (auth) throw redirect("/dashboard");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function action({ request }: Route.ActionArgs) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const result = registerSchema.safeParse({
|
||||||
|
username: formData.get("username"),
|
||||||
|
firstName: formData.get("firstName"),
|
||||||
|
lastName: formData.get("lastName"),
|
||||||
|
email: formData.get("email"),
|
||||||
|
dateOfBirth: formData.get("dateOfBirth"),
|
||||||
|
password: formData.get("password"),
|
||||||
|
confirmPassword: formData.get("confirmPassword"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const fieldErrors = result.error.flatten().fieldErrors;
|
||||||
|
return { error: null, fieldErrors };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { confirmPassword: _, ...body } = result.data;
|
||||||
|
const payload = await register(body);
|
||||||
|
return createAuthSession(payload, "/dashboard");
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
error: err instanceof Error ? err.message : "Registration failed.",
|
||||||
|
fieldErrors: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Register({ actionData }: Route.ComponentProps) {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const submit = useSubmit();
|
||||||
|
const isSubmitting = navigation.state === "submitting";
|
||||||
|
|
||||||
|
const {
|
||||||
|
register: field,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<RegisterSchema>({ resolver: zodResolver(registerSchema) });
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
submit(data, { method: "post" });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-base-200 flex items-center justify-center p-4">
|
||||||
|
<div className="card w-full max-w-lg bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="card-title text-3xl justify-center">Register</h1>
|
||||||
|
<p className="text-base-content/70">
|
||||||
|
Create your Biergarten account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actionData?.error && (
|
||||||
|
<div role="alert" className="alert alert-error alert-soft">
|
||||||
|
<span>{actionData.error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit} className="space-y-2">
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Username</legend>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
placeholder="your_username"
|
||||||
|
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
||||||
|
{...field("username")}
|
||||||
|
/>
|
||||||
|
{errors.username ? (
|
||||||
|
<p className="label text-error">{errors.username.message}</p>
|
||||||
|
) : (
|
||||||
|
<p className="label">3-64 characters, alphanumeric and . _ -</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">First Name</legend>
|
||||||
|
<input
|
||||||
|
id="firstName"
|
||||||
|
type="text"
|
||||||
|
autoComplete="given-name"
|
||||||
|
placeholder="Jane"
|
||||||
|
className={`input w-full ${errors.firstName ? "input-error" : ""}`}
|
||||||
|
{...field("firstName")}
|
||||||
|
/>
|
||||||
|
{errors.firstName && (
|
||||||
|
<p className="label text-error">{errors.firstName.message}</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Last Name</legend>
|
||||||
|
<input
|
||||||
|
id="lastName"
|
||||||
|
type="text"
|
||||||
|
autoComplete="family-name"
|
||||||
|
placeholder="Doe"
|
||||||
|
className={`input w-full ${errors.lastName ? "input-error" : ""}`}
|
||||||
|
{...field("lastName")}
|
||||||
|
/>
|
||||||
|
{errors.lastName && (
|
||||||
|
<p className="label text-error">{errors.lastName.message}</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Email</legend>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
placeholder="jane@example.com"
|
||||||
|
className={`input w-full ${errors.email ? "input-error" : ""}`}
|
||||||
|
{...field("email")}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="label text-error">{errors.email.message}</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Date of Birth</legend>
|
||||||
|
<input
|
||||||
|
id="dateOfBirth"
|
||||||
|
type="date"
|
||||||
|
className={`input w-full ${errors.dateOfBirth ? "input-error" : ""}`}
|
||||||
|
{...field("dateOfBirth")}
|
||||||
|
/>
|
||||||
|
{errors.dateOfBirth ? (
|
||||||
|
<p className="label text-error">{errors.dateOfBirth.message}</p>
|
||||||
|
) : (
|
||||||
|
<p className="label">Must be 19 years or older</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Password</legend>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
||||||
|
{...field("password")}
|
||||||
|
/>
|
||||||
|
{errors.password ? (
|
||||||
|
<p className="label text-error">{errors.password.message}</p>
|
||||||
|
) : (
|
||||||
|
<p className="label">
|
||||||
|
8+ chars: uppercase, lowercase, digit, special character
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset className="fieldset">
|
||||||
|
<legend className="fieldset-legend">Confirm Password</legend>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
className={`input w-full ${errors.confirmPassword ? "input-error" : ""}`}
|
||||||
|
{...field("confirmPassword")}
|
||||||
|
/>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<p className="label text-error">
|
||||||
|
{errors.confirmPassword.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="btn btn-primary w-full mt-2"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<span className="loading loading-spinner loading-sm" />{" "}
|
||||||
|
Creating account...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Create Account"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="divider text-xs">Already have an account?</div>
|
||||||
|
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<Link to="/login" className="btn btn-outline btn-sm w-full">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="link link-hover text-sm text-base-content/60"
|
||||||
|
>
|
||||||
|
← Back to home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3816
src/Website/package-lock.json
generated
Normal file
3816
src/Website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
src/Website/package.json
Normal file
36
src/Website/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "biergarten-website",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "react-router dev",
|
||||||
|
"build": "react-router build",
|
||||||
|
"start": "NODE_ENV=production node ./build/server/index.js",
|
||||||
|
"typecheck": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.4.0",
|
||||||
|
"@react-router/dev": "latest",
|
||||||
|
"@react-router/express": "latest",
|
||||||
|
"@react-router/node": "latest",
|
||||||
|
"isbot": "^5",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.51.4",
|
||||||
|
"react-router": "latest",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.16.5",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"daisyui": "latest",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vite": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/Website/postcss.config.js
Normal file
5
src/Website/postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
5
src/Website/react-router.config.ts
Normal file
5
src/Website/react-router.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { Config } from "@react-router/dev/config";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
ssr: true,
|
||||||
|
} satisfies Config;
|
||||||
8
src/Website/tailwind.config.js
Normal file
8
src/Website/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./app/**/*.{ts,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("daisyui")],
|
||||||
|
};
|
||||||
18
src/Website/tsconfig.json
Normal file
18
src/Website/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"charset": "utf8",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"module": "ESNext",
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["app"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
6
src/Website/vite.config.ts
Normal file
6
src/Website/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { reactRouter } from "@react-router/dev/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [reactRouter()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user