diff --git a/copier.yaml b/copier.yaml
index f7625ca..95917ff 100644
--- a/copier.yaml
+++ b/copier.yaml
@@ -37,6 +37,17 @@ keyprefix:
help: The prefix for various keys used in the app
default: "{{ appname | lower | replace('_', '-') }}"
+identity:
+ type: bool
+ help: Whether to include identity features in the app
+ default: true
+
+access:
+ type: bool
+ help: Whether to include access features in the app
+ default: true
+ when: "{{ identity }}"
+
docs:
type: bool
help: Whether to include a documentation site for the app
diff --git a/src/docker-compose.yaml.jinja b/src/docker-compose.yaml.jinja
index c4901a7..68784c5 100644
--- a/src/docker-compose.yaml.jinja
+++ b/src/docker-compose.yaml.jinja
@@ -9,6 +9,9 @@ services:
- "{{ envprefix }}__APIS__ICANHAZDADJOKE__PORT=${ {{- envprefix }}__APIS__ICANHAZDADJOKE__PORT:-}"
- "{{ envprefix }}__APIS__ICANHAZDADJOKE__SCHEME=${ {{- envprefix }}__APIS__ICANHAZDADJOKE__SCHEME:-https}"
- "{{ envprefix }}__DEBUG=${ {{- envprefix }}__DEBUG:-true}"
+ {%- if identity %}
+ - "{{ envprefix }}__IDENTITY__USERS__DEBUG__ID=${ {{- envprefix }}__IDENTITY__USERS__DEBUG__ID:-debug}"
+ {%- endif %}
- "{{ envprefix }}__SERVER__HOST=${ {{- envprefix }}__SERVER__HOST:-0.0.0.0}"
- "{{ envprefix }}__SERVER__PORT=${ {{- envprefix }}__SERVER__PORT:-{{ port -}} }"
network_mode: host
diff --git a/src/next.config.ts b/src/next.config.ts
index 9c6237c..0403805 100644
--- a/src/next.config.ts
+++ b/src/next.config.ts
@@ -13,6 +13,9 @@ export default {
distDir: "build",
experimental: {
+ // Enable authentication and authorization interrupts
+ authInterrupts: true,
+
// Optimize import from Mantine packages
optimizePackageImports: [
"@mantine/core",
diff --git a/src/src/app/(root)/(main)/(home)/page.tsx.jinja b/src/src/app/(root)/(main)/(home)/page.tsx.jinja
index ea6e8eb..0bfa7ee 100644
--- a/src/src/app/(root)/(main)/(home)/page.tsx.jinja
+++ b/src/src/app/(root)/(main)/(home)/page.tsx.jinja
@@ -9,6 +9,9 @@ import type {
import type { Keys } from "./types";
import { Metadata } from "../../../../isomorphic/metadata/components/metadata";
+{%- if identity and access %}
+import { Authenticated } from "../../../../server/access/components/authenticated";
+{%- endif %}
import { createMetadata } from "../../../../server/metadata/lib/create-metadata";
import { HomePageView } from "./page.view";
import { Schemas } from "./schemas";
@@ -38,9 +41,9 @@ export default async function HomePage({
const queryParameters = await Schemas.Query.parseAsync(await searchParams);
return (
- <>
+ <{% if identity and access %}Authenticated{% endif %}>
- >
+ {% if identity and access %}Authenticated{% endif %}>
);
}
diff --git a/src/src/app/(root)/layout.tsx.jinja b/src/src/app/(root)/layout.tsx.jinja
index 5e1b0ec..4be80d5 100644
--- a/src/src/app/(root)/layout.tsx.jinja
+++ b/src/src/app/(root)/layout.tsx.jinja
@@ -14,12 +14,18 @@ import type { Schemas } from "./schemas";
import type { Keys } from "./types";
import { ThemeScript } from "../../common/theme/components/theme-script";
+{%- if identity %}
+import { IdentityProvider } from "../../isomorphic/identity/components/identity-provider";
+{%- endif %}
import { LocalizationProvider } from "../../isomorphic/localization/components/localization-provider";
import { Metadata } from "../../isomorphic/metadata/components/metadata";
import { MetadataProvider } from "../../isomorphic/metadata/components/metadata-provider";
import { QueryProvider } from "../../isomorphic/query/components/query-provider";
import { StateProvider } from "../../isomorphic/state/components/state-provider";
import { ThemeProvider } from "../../isomorphic/theme/components/theme-provider";
+{%- if identity %}
+import { getIdentity } from "../../server/identity/lib/get-identity";
+{%- endif %}
import { resolveLocale } from "../../server/localization/lib/resolve-locale";
import { createMetadata } from "../../server/metadata/lib/create-metadata";
import { createViewport } from "../../server/metadata/lib/create-viewport";
@@ -80,6 +86,9 @@ export default async function RootLayout({
const { queryClient } = getQueryClient();
const { locale } = await resolveLocale({ queryClient: queryClient });
+ {%- if identity %}
+ const { identity } = await getIdentity({ queryClient: queryClient });
+ {%- endif %}
return (
@@ -97,11 +106,21 @@ export default async function RootLayout({
primaryColor={constants.colors.primary.name}
primaryShade={constants.colors.primary.shade}
>
+ {%- if identity %}
+
+
+ {children}
+
+ {%- else %}
{children}
+ {%- endif %}
diff --git a/src/src/app/(root)/layout.view.tsx b/src/src/app/(root)/layout.view.tsx
deleted file mode 100644
index 793d7dd..0000000
--- a/src/src/app/(root)/layout.view.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { LayoutViewInput } from "../types";
-import type { Schemas } from "./schemas";
-import type { Keys } from "./types";
-
-export async function RootLayoutView({
- children,
-}: LayoutViewInput) {
- return children;
-}
diff --git a/src/src/app/(root)/layout.view.tsx.jinja b/src/src/app/(root)/layout.view.tsx.jinja
new file mode 100644
index 0000000..70ef89e
--- /dev/null
+++ b/src/src/app/(root)/layout.view.tsx.jinja
@@ -0,0 +1,23 @@
+import type { LayoutViewInput } from "../types";
+import type { Schemas } from "./schemas";
+import type { Keys } from "./types";
+
+import { PageLayout } from "../../common/core/components/generic/page-layout";
+{%- if identity %}
+import { IdentityWidget } from "../../isomorphic/identity/components/identity-widget";
+{%- endif %}
+
+export async function RootLayoutView({
+ children,
+}: LayoutViewInput) {
+ {%- if identity %}
+ return (
+
+
+ {children}
+
+ );
+ {%- else %}
+ return {children};
+ {%- endif %}
+}
diff --git a/src/src/app/(root)/{% if identity and access %}forbidden.tsx{% endif %}.jinja b/src/src/app/(root)/{% if identity and access %}forbidden.tsx{% endif %}.jinja
new file mode 100644
index 0000000..f18f5b6
--- /dev/null
+++ b/src/src/app/(root)/{% if identity and access %}forbidden.tsx{% endif %}.jinja
@@ -0,0 +1,21 @@
+"use client";
+
+import { msg } from "@lingui/core/macro";
+
+import type { ForbiddenInput, ForbiddenMetadataUtilityInput } from "../types";
+
+import { Metadata } from "../../isomorphic/metadata/components/metadata";
+import { RootForbiddenView } from "./forbidden.view";
+
+function getTitle({}: ForbiddenMetadataUtilityInput = {}) {
+ return msg({ message: "Forbidden • {{ appname }}" });
+}
+
+export default function RootForbidden({}: ForbiddenInput) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/src/app/(root)/{% if identity and access %}forbidden.view.tsx{% endif %} b/src/src/app/(root)/{% if identity and access %}forbidden.view.tsx{% endif %}
new file mode 100644
index 0000000..7031994
--- /dev/null
+++ b/src/src/app/(root)/{% if identity and access %}forbidden.view.tsx{% endif %}
@@ -0,0 +1,12 @@
+import type { ForbiddenViewInput } from "../types";
+
+import { MainLayout } from "../../common/core/components/generic/main-layout";
+import { ForbiddenWidget } from "../../isomorphic/access/components/forbidden-widget";
+
+export function RootForbiddenView({}: ForbiddenViewInput) {
+ return (
+
+
+
+ );
+}
diff --git a/src/src/app/global-error.view.tsx b/src/src/app/global-error.view.tsx
index 380ef6a..ef19770 100644
--- a/src/src/app/global-error.view.tsx
+++ b/src/src/app/global-error.view.tsx
@@ -1,12 +1,15 @@
import type { ErrorViewInput } from "./types";
import { SafeMainLayout } from "../common/core/components/generic/safe-main-layout";
+import { SafePageLayout } from "../common/core/components/generic/safe-page-layout";
import { SafeErrorWidget } from "../isomorphic/core/components/generic/safe-error-widget";
export function GlobalErrorView({ reset }: ErrorViewInput) {
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/src/src/app/types.ts b/src/src/app/types.ts
index 897e286..d886634 100644
--- a/src/src/app/types.ts
+++ b/src/src/app/types.ts
@@ -48,6 +48,8 @@ export type ErrorInput = {
reset: () => void;
};
+export type ForbiddenInput = object;
+
export type LayoutInput<
PathParametersKeysType extends string = never,
SlotsKeysType extends string = never,
@@ -77,8 +79,12 @@ export type RouteInput = {
export type TemplateInput = Simplify>;
+export type UnauthorizedInput = object;
+
export type ErrorMetadataUtilityInput = object;
+export type ForbiddenMetadataUtilityInput = object;
+
export type LayoutMetadataInput =
{
params: Promise>;
@@ -120,8 +126,12 @@ export type PageMetadataUtilityInput<
})
>;
+export type UnauthorizedMetadataUtilityInput = object;
+
export type ErrorViewportUtilityInput = object;
+export type ForbiddenViewportUtilityInput = object;
+
export type LayoutViewportInput =
{
params: Promise>;
@@ -163,6 +173,8 @@ export type PageViewportUtilityInput<
})
>;
+export type UnauthorizedViewportUtilityInput = object;
+
export type DefaultViewInput<
PathParametersSchemaType extends z.core.$ZodObject = never,
> = [PathParametersSchemaType] extends [never]
@@ -173,6 +185,8 @@ export type ErrorViewInput = {
reset: () => void;
};
+export type ForbiddenViewInput = object;
+
export type LayoutViewInput<
PathParametersSchemaType extends z.core.$ZodObject = never,
SlotsKeysType extends string = never,
@@ -208,3 +222,5 @@ export type PageViewInput<
>;
export type TemplateViewInput = Simplify>;
+
+export type UnauthorizedViewInput = object;
diff --git a/src/src/common/core/components/generic/main-layout/main.tsx b/src/src/common/core/components/generic/main-layout/main.tsx
index a27183a..7d6784c 100644
--- a/src/src/common/core/components/generic/main-layout/main.tsx
+++ b/src/src/common/core/components/generic/main-layout/main.tsx
@@ -1,17 +1,9 @@
import { Box } from "@mantine/core";
-import { Notifications } from "@mantine/notifications";
import type { MainLayoutInput } from "./types";
import classes from "./styles.module.css";
export function MainLayout({ children }: MainLayoutInput) {
- return (
- <>
-
-
- {children}
-
- >
- );
+ return {children};
}
diff --git a/src/src/common/core/components/generic/main-layout/styles.module.css b/src/src/common/core/components/generic/main-layout/styles.module.css
index 10b0f7b..354c5c2 100644
--- a/src/src/common/core/components/generic/main-layout/styles.module.css
+++ b/src/src/common/core/components/generic/main-layout/styles.module.css
@@ -1,14 +1,4 @@
-.outer {
- background-color: light-dark(
- var(--mantine-color-gray-1),
- var(--mantine-color-dark-7)
- );
- height: 100%;
- padding: 0;
- width: 100%;
-}
-
-.inner {
+.box {
align-items: center;
display: flex;
flex-direction: column;
diff --git a/src/src/common/core/components/generic/page-layout/index.ts b/src/src/common/core/components/generic/page-layout/index.ts
new file mode 100644
index 0000000..4127eda
--- /dev/null
+++ b/src/src/common/core/components/generic/page-layout/index.ts
@@ -0,0 +1,2 @@
+export { PageLayout } from "./main";
+export type { PageLayoutInput } from "./types";
diff --git a/src/src/common/core/components/generic/page-layout/main.tsx b/src/src/common/core/components/generic/page-layout/main.tsx
new file mode 100644
index 0000000..3bc0b6c
--- /dev/null
+++ b/src/src/common/core/components/generic/page-layout/main.tsx
@@ -0,0 +1,15 @@
+import { Box } from "@mantine/core";
+import { Notifications } from "@mantine/notifications";
+
+import type { PageLayoutInput } from "./types";
+
+import classes from "./styles.module.css";
+
+export function PageLayout({ children }: PageLayoutInput) {
+ return (
+ <>
+
+ {children}
+ >
+ );
+}
diff --git a/src/src/common/core/components/generic/page-layout/styles.module.css b/src/src/common/core/components/generic/page-layout/styles.module.css
new file mode 100644
index 0000000..ab3dbb0
--- /dev/null
+++ b/src/src/common/core/components/generic/page-layout/styles.module.css
@@ -0,0 +1,9 @@
+.box {
+ background-color: light-dark(
+ var(--mantine-color-gray-1),
+ var(--mantine-color-dark-7)
+ );
+ height: 100%;
+ padding: 0;
+ width: 100%;
+}
diff --git a/src/src/common/core/components/generic/page-layout/types.ts b/src/src/common/core/components/generic/page-layout/types.ts
new file mode 100644
index 0000000..6a966f0
--- /dev/null
+++ b/src/src/common/core/components/generic/page-layout/types.ts
@@ -0,0 +1,3 @@
+import type { PropsWithChildren } from "react";
+
+export type PageLayoutInput = PropsWithChildren