@cosmicdrift/kumiko-bundled-features 0.87.0 → 0.87.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/auth-email-password/web/__tests__/session-auth-bootstrap.test.ts +22 -0
- package/src/auth-email-password/web/auth-gate.tsx +20 -1
- package/src/auth-email-password/web/client-plugin.ts +3 -4
- package/src/auth-email-password/web/index.ts +2 -2
- package/src/auth-email-password/web/session.tsx +21 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.87.
|
|
3
|
+
"version": "0.87.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.87.
|
|
90
|
-
"@cosmicdrift/kumiko-framework": "0.87.
|
|
91
|
-
"@cosmicdrift/kumiko-headless": "0.87.
|
|
92
|
-
"@cosmicdrift/kumiko-renderer": "0.87.
|
|
93
|
-
"@cosmicdrift/kumiko-renderer-web": "0.87.
|
|
89
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.87.1",
|
|
90
|
+
"@cosmicdrift/kumiko-framework": "0.87.1",
|
|
91
|
+
"@cosmicdrift/kumiko-headless": "0.87.1",
|
|
92
|
+
"@cosmicdrift/kumiko-renderer": "0.87.1",
|
|
93
|
+
"@cosmicdrift/kumiko-renderer-web": "0.87.1",
|
|
94
94
|
"@mollie/api-client": "^4.5.0",
|
|
95
95
|
"@node-rs/argon2": "^2.0.2",
|
|
96
96
|
"@types/nodemailer": "^8.0.0",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { emailPasswordClient } from "../client-plugin";
|
|
3
|
+
import { hasLikelyAuthSession } from "../session";
|
|
4
|
+
|
|
5
|
+
describe("hasLikelyAuthSession", () => {
|
|
6
|
+
test("no kumiko_csrf cookie → false", () => {
|
|
7
|
+
expect(hasLikelyAuthSession("theme=dark")).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("kumiko_csrf present → true", () => {
|
|
11
|
+
expect(hasLikelyAuthSession("kumiko_csrf=abc-123")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("emailPasswordClient", () => {
|
|
16
|
+
test("registers SessionAuthGate as gate, not SessionProvider as provider", () => {
|
|
17
|
+
const feature = emailPasswordClient();
|
|
18
|
+
expect(feature.providers).toEqual([]);
|
|
19
|
+
expect(feature.gates).toHaveLength(1);
|
|
20
|
+
expect(feature.gates[0]?.name).toBe("SessionAuthGate");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ComponentType, ReactNode } from "react";
|
|
13
13
|
import { LoginScreen, type LoginScreenProps } from "./login-screen";
|
|
14
|
-
import { useSession } from "./session";
|
|
14
|
+
import { SessionProvider, useSession } from "./session";
|
|
15
15
|
|
|
16
16
|
export function makeAuthGate(
|
|
17
17
|
LoginComponent: ComponentType<LoginScreenProps> = LoginScreen,
|
|
@@ -31,3 +31,22 @@ export function makeAuthGate(
|
|
|
31
31
|
}
|
|
32
32
|
return AuthGate;
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** SessionProvider + AuthGate als ein Gate — damit öffentliche Gates davor
|
|
36
|
+
* (z.B. /rechner) den Session-Bootstrap nicht mounten. createKumikoApp
|
|
37
|
+
* stackt providers außerhalb aller gates; SessionProvider darf deshalb
|
|
38
|
+
* kein provider mehr sein. */
|
|
39
|
+
export function makeSessionAuthGate(
|
|
40
|
+
LoginComponent: ComponentType<LoginScreenProps> = LoginScreen,
|
|
41
|
+
loginProps?: LoginScreenProps,
|
|
42
|
+
): ComponentType<{ children: ReactNode }> {
|
|
43
|
+
const AuthGate = makeAuthGate(LoginComponent, loginProps);
|
|
44
|
+
function SessionAuthGate({ children }: { readonly children: ReactNode }): ReactNode {
|
|
45
|
+
return (
|
|
46
|
+
<SessionProvider>
|
|
47
|
+
<AuthGate>{children}</AuthGate>
|
|
48
|
+
</SessionProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return SessionAuthGate;
|
|
52
|
+
}
|
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
9
9
|
import type { ComponentType, ReactNode } from "react";
|
|
10
10
|
import { defaultTranslations, mergeTranslations } from "../i18n";
|
|
11
|
-
import {
|
|
11
|
+
import { makeSessionAuthGate } from "./auth-gate";
|
|
12
12
|
import type { LoginScreenProps } from "./login-screen";
|
|
13
|
-
import { SessionProvider } from "./session";
|
|
14
13
|
|
|
15
14
|
export type EmailPasswordClientOptions = {
|
|
16
15
|
/** Eigener Login-Screen. Default: der shadcn-stylte LoginScreen
|
|
@@ -41,8 +40,8 @@ export function emailPasswordClient(
|
|
|
41
40
|
const translations = mergeTranslations(defaultTranslations, options.translations ?? {});
|
|
42
41
|
return {
|
|
43
42
|
name: "auth-email-password",
|
|
44
|
-
providers: [
|
|
45
|
-
gates: [
|
|
43
|
+
providers: [],
|
|
44
|
+
gates: [makeSessionAuthGate(options.loginScreen, options.loginScreenProps)],
|
|
46
45
|
translations,
|
|
47
46
|
};
|
|
48
47
|
}
|
|
@@ -26,7 +26,7 @@ export {
|
|
|
26
26
|
} from "./auth-client";
|
|
27
27
|
export type { AuthShellRenderer } from "./auth-form-primitives";
|
|
28
28
|
export { AuthShellProvider, useAuthShell } from "./auth-form-primitives";
|
|
29
|
-
export { makeAuthGate } from "./auth-gate";
|
|
29
|
+
export { makeAuthGate, makeSessionAuthGate } from "./auth-gate";
|
|
30
30
|
export type {
|
|
31
31
|
EmailPasswordClientFeature,
|
|
32
32
|
EmailPasswordClientOptions,
|
|
@@ -43,7 +43,7 @@ export { LoginScreen } from "./login-screen";
|
|
|
43
43
|
export type { ResetPasswordScreenProps } from "./reset-password-screen";
|
|
44
44
|
export { ResetPasswordScreen } from "./reset-password-screen";
|
|
45
45
|
export type { SessionApi, SessionState, SessionStatus } from "./session";
|
|
46
|
-
export { SessionContext, SessionProvider, useSession } from "./session";
|
|
46
|
+
export { hasLikelyAuthSession, SessionContext, SessionProvider, useSession } from "./session";
|
|
47
47
|
export type { SignupCompleteScreenProps } from "./signup-complete-screen";
|
|
48
48
|
export { SignupCompleteScreen } from "./signup-complete-screen";
|
|
49
49
|
export type { SignupScreenProps } from "./signup-screen";
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// unter `<SessionProvider>`; der `useSession()`-Hook liefert den State
|
|
10
10
|
// und die Transitions.
|
|
11
11
|
|
|
12
|
+
import { readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
12
13
|
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
|
|
13
14
|
import {
|
|
14
15
|
type CurrentUserProfile,
|
|
@@ -43,6 +44,14 @@ export type SessionApi = SessionState & {
|
|
|
43
44
|
readonly switchTenant: (tenantId: string) => Promise<void>;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const UNAUTHENTICATED: SessionState = {
|
|
48
|
+
status: "unauthenticated",
|
|
49
|
+
user: null,
|
|
50
|
+
activeTenantId: null,
|
|
51
|
+
tenants: [],
|
|
52
|
+
roles: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
46
55
|
const INITIAL: SessionState = {
|
|
47
56
|
status: "loading",
|
|
48
57
|
user: null,
|
|
@@ -51,6 +60,11 @@ const INITIAL: SessionState = {
|
|
|
51
60
|
roles: [],
|
|
52
61
|
};
|
|
53
62
|
|
|
63
|
+
// kumiko_auth ist HttpOnly — kumiko_csrf wird beim Login gemeinsam gesetzt.
|
|
64
|
+
export function hasLikelyAuthSession(cookieSource?: string): boolean {
|
|
65
|
+
return readCsrfToken(cookieSource) !== undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
// Exported damit tests den merge-pfad direkt pinnen können — der hier
|
|
55
69
|
// muss byte-identisch zum server-side merge in auth-routes.ts +
|
|
56
70
|
// login.write.ts sein, sonst sieht der Client andere session-rollen
|
|
@@ -76,27 +90,18 @@ export function computeActiveRoles(
|
|
|
76
90
|
export const SessionContext = createContext<SessionApi | undefined>(undefined);
|
|
77
91
|
|
|
78
92
|
// Eine Refresh-Runde: /auth/tenants → wenn 401 nicht-eingeloggt, sonst
|
|
79
|
-
//
|
|
93
|
+
// /user:me. Beides zusammen ergibt den vollen SessionState.
|
|
80
94
|
async function refresh(): Promise<SessionState> {
|
|
95
|
+
if (!hasLikelyAuthSession()) {
|
|
96
|
+
return UNAUTHENTICATED;
|
|
97
|
+
}
|
|
81
98
|
const tenants = await fetchTenants();
|
|
82
99
|
if (tenants === null) {
|
|
83
|
-
return
|
|
84
|
-
status: "unauthenticated",
|
|
85
|
-
user: null,
|
|
86
|
-
activeTenantId: null,
|
|
87
|
-
tenants: [],
|
|
88
|
-
roles: [],
|
|
89
|
-
};
|
|
100
|
+
return UNAUTHENTICATED;
|
|
90
101
|
}
|
|
91
102
|
const user = await fetchCurrentUser();
|
|
92
103
|
if (user === null) {
|
|
93
|
-
return
|
|
94
|
-
status: "unauthenticated",
|
|
95
|
-
user: null,
|
|
96
|
-
activeTenantId: null,
|
|
97
|
-
tenants: [],
|
|
98
|
-
roles: [],
|
|
99
|
-
};
|
|
104
|
+
return UNAUTHENTICATED;
|
|
100
105
|
}
|
|
101
106
|
return {
|
|
102
107
|
status: "authenticated",
|
|
@@ -131,13 +136,7 @@ export function SessionProvider({ children }: { readonly children: ReactNode }):
|
|
|
131
136
|
|
|
132
137
|
const logout = useCallback<SessionApi["logout"]>(async () => {
|
|
133
138
|
await logoutApi();
|
|
134
|
-
setState(
|
|
135
|
-
status: "unauthenticated",
|
|
136
|
-
user: null,
|
|
137
|
-
activeTenantId: null,
|
|
138
|
-
tenants: [],
|
|
139
|
-
roles: [],
|
|
140
|
-
});
|
|
139
|
+
setState(UNAUTHENTICATED);
|
|
141
140
|
// Hard-Reload: React-Tree, dispatcher-live-Caches, EventSource —
|
|
142
141
|
// alles fliegt auf Null. Nach Logout ist das der billigste Weg zu
|
|
143
142
|
// sauberer Ausgangslage, ohne dass wir jeden einzelnen Consumer
|