autherr 1.0.0

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/README.md ADDED
Binary file
@@ -0,0 +1 @@
1
+ export declare function logoutSession(baseUrl: string, clientId: string): Promise<void>;
@@ -0,0 +1,6 @@
1
+ export async function logoutSession(baseUrl, clientId) {
2
+ await fetch(`${baseUrl}/__bff/auth/logout?client_id=${clientId}`, {
3
+ method: "POST",
4
+ credentials: "include", // REQUIRED to clear cookie
5
+ });
6
+ }
@@ -0,0 +1,2 @@
1
+ import { AutherrSessionResponse } from "../types/auth";
2
+ export declare function fetchSession(baseUrl: string, clientId: string): Promise<AutherrSessionResponse>;
@@ -0,0 +1,66 @@
1
+ export async function fetchSession(baseUrl, clientId) {
2
+ const res = await fetch(`${baseUrl}/__bff/auth/session?client_id=${clientId}`, {
3
+ method: "GET",
4
+ credentials: "include", // REQUIRED for refresh cookie
5
+ headers: {
6
+ "Content-Type": "application/json",
7
+ },
8
+ });
9
+ if (!res.ok) {
10
+ return { authenticated: false };
11
+ }
12
+ const json = await res.json();
13
+ /**
14
+ * Backend response shapes:
15
+ * 1. Not authenticated:
16
+ * { success: false, authenticated: false }
17
+ *
18
+ * 2. Authenticated:
19
+ * {
20
+ * success: true,
21
+ * authenticated: true,
22
+ * data: {
23
+ * userId,
24
+ * accessToken,
25
+ * expiresIn
26
+ * }
27
+ * }
28
+ */
29
+ if (!json.authenticated || !json.data?.accessToken) {
30
+ return { authenticated: false };
31
+ }
32
+ return {
33
+ authenticated: true,
34
+ accessToken: json.data.accessToken,
35
+ expiresAt: Date.now() + json.data.expiresIn * 1000,
36
+ };
37
+ }
38
+ // import { AutherrSessionResponse } from "../types/auth";
39
+ // export async function fetchSession(
40
+ // baseUrl: string,
41
+ // clientId: string
42
+ // ): Promise<AutherrSessionResponse> {
43
+ // const res = await fetch(
44
+ // `${baseUrl}/__bff/auth/session?client_id=${clientId}`,
45
+ // {
46
+ // method: "GET",
47
+ // credentials: "include", // CRITICAL
48
+ // headers: {
49
+ // "Content-Type": "application/json",
50
+ // },
51
+ // }
52
+ // );
53
+ // if (!res.ok) {
54
+ // return { authenticated: false };
55
+ // }
56
+ // const data = (await res.json()) as AutherrSessionResponse;
57
+ // if (!data || !data.accessToken) {
58
+ // return { authenticated: false };
59
+ // }
60
+ // return {
61
+ // authenticated: true,
62
+ // user: data.user,
63
+ // accessToken: data.accessToken,
64
+ // expiresAt: data.expiresAt,
65
+ // };
66
+ // }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ interface LoginButtonProps {
3
+ children?: React.ReactNode;
4
+ className?: string;
5
+ }
6
+ export declare function LoginButton({ children, className, }: LoginButtonProps): import("react/jsx-runtime").JSX.Element | null;
7
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAutherr } from "../hooks/useAutherr";
3
+ export function LoginButton({ children = "Sign in", className, }) {
4
+ const { login, isAuthenticated } = useAutherr();
5
+ if (isAuthenticated)
6
+ return null;
7
+ return (_jsx("button", { type: "button", onClick: login, className: className, children: children }));
8
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ interface SignupButtonProps {
3
+ children?: React.ReactNode;
4
+ className?: string;
5
+ }
6
+ export declare function SignupButton({ children, className, }: SignupButtonProps): import("react/jsx-runtime").JSX.Element | null;
7
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAutherr } from "../hooks/useAutherr";
3
+ export function SignupButton({ children = "Sign up", className, }) {
4
+ const { signup, isAuthenticated } = useAutherr();
5
+ if (isAuthenticated)
6
+ return null;
7
+ return (_jsx("button", { type: "button", onClick: signup, className: className, children: children }));
8
+ }
@@ -0,0 +1 @@
1
+ export declare function UserMenu(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { useAutherr } from "../hooks/useAutherr";
4
+ // simple local decoder (no verification)
5
+ function decodeJwt(token) {
6
+ try {
7
+ const [, payload] = token.split(".");
8
+ return JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function UserMenu() {
15
+ const { isAuthenticated, accessToken, login, signup, logout, } = useAutherr();
16
+ const [open, setOpen] = useState(false);
17
+ const [showPayload, setShowPayload] = useState(false);
18
+ const ref = useRef(null);
19
+ // close on outside click
20
+ useEffect(() => {
21
+ function handleClick(e) {
22
+ if (ref.current && !ref.current.contains(e.target)) {
23
+ setOpen(false);
24
+ setShowPayload(false);
25
+ }
26
+ }
27
+ document.addEventListener("mousedown", handleClick);
28
+ return () => document.removeEventListener("mousedown", handleClick);
29
+ }, []);
30
+ const payload = accessToken ? decodeJwt(accessToken) : null;
31
+ const initial = payload?.email?.[0]?.toUpperCase() ?? "U";
32
+ return (_jsxs("div", { ref: ref, style: { position: "relative" }, children: [_jsx("button", { onClick: () => setOpen((v) => !v), style: {
33
+ width: 36,
34
+ height: 36,
35
+ borderRadius: "50%",
36
+ background: "#ff6a00",
37
+ color: "#fff",
38
+ border: "none",
39
+ cursor: "pointer",
40
+ fontWeight: 600,
41
+ }, children: isAuthenticated ? initial : "?" }), open && (_jsx("div", { style: {
42
+ position: "absolute",
43
+ top: "calc(100% + 8px)",
44
+ right: 0,
45
+ width: 280,
46
+ background: "#fff",
47
+ borderRadius: 12,
48
+ boxShadow: "0 10px 30px rgba(0,0,0,0.15)",
49
+ padding: 12,
50
+ zIndex: 1000,
51
+ }, children: isAuthenticated ? (_jsxs(_Fragment, { children: [_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontWeight: 600 }, children: payload?.email }), _jsx("div", { style: { fontSize: 12, color: "#666" }, children: "Signed in" })] }), _jsx(Divider, {}), _jsx(MenuItem, { onClick: () => setShowPayload((v) => !v), children: showPayload ? "Hide payload" : "View payload" }), _jsx(MenuItem, { onClick: logout, children: "Sign out" }), showPayload && payload && (_jsx("pre", { style: {
52
+ marginTop: 10,
53
+ background: "#0b0b0b",
54
+ color: "#00ff88",
55
+ padding: 10,
56
+ borderRadius: 8,
57
+ fontSize: 12,
58
+ maxHeight: 200,
59
+ overflow: "auto",
60
+ }, children: JSON.stringify(payload, null, 2) }))] })) : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onClick: login, children: "Sign in" }), _jsx(MenuItem, { onClick: signup, children: "Sign up" })] })) }))] }));
61
+ }
62
+ function MenuItem({ children, onClick, }) {
63
+ return (_jsx("button", { onClick: onClick, style: {
64
+ width: "100%",
65
+ padding: "8px 0",
66
+ background: "none",
67
+ border: "none",
68
+ textAlign: "left",
69
+ cursor: "pointer",
70
+ fontSize: 14,
71
+ }, children: children }));
72
+ }
73
+ function Divider() {
74
+ return (_jsx("div", { style: {
75
+ height: 1,
76
+ background: "#eee",
77
+ margin: "8px 0",
78
+ } }));
79
+ }
@@ -0,0 +1,10 @@
1
+ export declare function useAutherr(): {
2
+ user: import("../types/auth").AutherrUser | null;
3
+ accessToken: string | null;
4
+ isAuthenticated: boolean;
5
+ login: () => void;
6
+ signup: () => void;
7
+ logout: () => Promise<void>;
8
+ refreshSession: () => Promise<void>;
9
+ getAccessToken: () => string | null;
10
+ };
@@ -0,0 +1,14 @@
1
+ import { useAutherrContext } from "../provider/AutherrProvider";
2
+ export function useAutherr() {
3
+ const { user, accessToken, isAuthenticated, login, signup, logout, getAccessToken, refreshSession, } = useAutherrContext();
4
+ return {
5
+ user,
6
+ accessToken,
7
+ isAuthenticated,
8
+ login,
9
+ signup,
10
+ logout,
11
+ refreshSession,
12
+ getAccessToken,
13
+ };
14
+ }
@@ -0,0 +1,5 @@
1
+ export { AutherrProvider } from "./provider/AutherrProvider";
2
+ export { useAutherr } from "./hooks/useAutherr";
3
+ export { LoginButton } from "./components/LoginButton";
4
+ export { SignupButton } from "./components/SignupButton";
5
+ export { UserMenu } from "./components/UserMenu";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { AutherrProvider } from "./provider/AutherrProvider";
2
+ export { useAutherr } from "./hooks/useAutherr";
3
+ export { LoginButton } from "./components/LoginButton";
4
+ export { SignupButton } from "./components/SignupButton";
5
+ export { UserMenu } from "./components/UserMenu";
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { AutherrUser } from "../types/auth";
3
+ interface AutherrContextValue {
4
+ user: AutherrUser | null;
5
+ accessToken: string | null;
6
+ isAuthenticated: boolean;
7
+ login: () => void;
8
+ signup: () => void;
9
+ logout: () => Promise<void>;
10
+ getAccessToken: () => string | null;
11
+ refreshSession: () => Promise<void>;
12
+ }
13
+ interface AutherrProviderProps {
14
+ children: React.ReactNode;
15
+ clientId: string;
16
+ baseUrl: string;
17
+ }
18
+ export declare function AutherrProvider({ children, clientId, baseUrl, }: AutherrProviderProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function useAutherrContext(): AutherrContextValue;
20
+ export {};
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useMemo, useState, } from "react";
3
+ import { fetchSession } from "../api/session";
4
+ import { logoutSession } from "../api/logout";
5
+ const AutherrContext = createContext(null);
6
+ export function AutherrProvider({ children, clientId, baseUrl, }) {
7
+ const [user, setUser] = useState(null);
8
+ const [accessToken, setAccessToken] = useState(null);
9
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
10
+ const refreshSession = async () => {
11
+ const session = await fetchSession(baseUrl, clientId);
12
+ if (session.authenticated && session.accessToken) {
13
+ setAccessToken(session.accessToken);
14
+ setIsAuthenticated(true);
15
+ }
16
+ else {
17
+ setUser(null);
18
+ setAccessToken(null);
19
+ setIsAuthenticated(false);
20
+ }
21
+ };
22
+ useEffect(() => {
23
+ refreshSession();
24
+ }, [baseUrl, clientId]);
25
+ const login = () => {
26
+ const state = crypto.randomUUID();
27
+ window.location.href =
28
+ `${baseUrl}/auth/login` +
29
+ `?client_id=${clientId}` +
30
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}` +
31
+ `&state=${state}`;
32
+ };
33
+ const signup = () => {
34
+ const state = crypto.randomUUID();
35
+ window.location.href =
36
+ `${baseUrl}/auth/signup` +
37
+ `?client_id=${clientId}` +
38
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}` +
39
+ `&state=${state}`;
40
+ };
41
+ const logout = async () => {
42
+ try {
43
+ await logoutSession(baseUrl, clientId);
44
+ }
45
+ finally {
46
+ setUser(null);
47
+ setAccessToken(null);
48
+ setIsAuthenticated(false);
49
+ window.location.href = window.location.origin;
50
+ }
51
+ };
52
+ const value = useMemo(() => ({
53
+ user,
54
+ accessToken,
55
+ isAuthenticated,
56
+ login,
57
+ signup,
58
+ logout,
59
+ refreshSession,
60
+ getAccessToken: () => accessToken,
61
+ }), [user, accessToken, isAuthenticated]);
62
+ return (_jsx(AutherrContext.Provider, { value: value, children: children }));
63
+ }
64
+ export function useAutherrContext() {
65
+ const ctx = useContext(AutherrContext);
66
+ if (!ctx) {
67
+ throw new Error("useAutherrContext must be used inside AutherrProvider");
68
+ }
69
+ return ctx;
70
+ }
@@ -0,0 +1,12 @@
1
+ export interface AutherrUser {
2
+ sub: string;
3
+ email: string;
4
+ userType: "platform" | "application";
5
+ applicationId?: string;
6
+ }
7
+ export interface AutherrSessionResponse {
8
+ authenticated: boolean;
9
+ user?: AutherrUser;
10
+ accessToken?: string;
11
+ expiresAt?: number;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "autherr",
3
+ "version": "1.0.0",
4
+ "dest": "dist",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json"
8
+ },
9
+ "dependencies": {
10
+ "react": "^19.2.3"
11
+ },
12
+ "devDependencies": {
13
+ "@types/react": "^19.2.8",
14
+ "@types/react-dom": "^19.2.3",
15
+ "typescript": "^5.9.3"
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ export async function logoutSession(
2
+ baseUrl: string,
3
+ clientId: string
4
+ ): Promise<void> {
5
+ await fetch(
6
+ `${baseUrl}/__bff/auth/logout?client_id=${clientId}`,
7
+ {
8
+ method: "POST",
9
+ credentials: "include", // REQUIRED to clear cookie
10
+ }
11
+ );
12
+ }
@@ -0,0 +1,91 @@
1
+ import { AutherrSessionResponse } from "../types/auth";
2
+
3
+ export async function fetchSession(
4
+ baseUrl: string,
5
+ clientId: string
6
+ ): Promise<AutherrSessionResponse> {
7
+ const res = await fetch(
8
+ `${baseUrl}/__bff/auth/session?client_id=${clientId}`,
9
+ {
10
+ method: "GET",
11
+ credentials: "include", // REQUIRED for refresh cookie
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ },
15
+ }
16
+ );
17
+
18
+ if (!res.ok) {
19
+ return { authenticated: false };
20
+ }
21
+
22
+ const json = await res.json();
23
+
24
+ /**
25
+ * Backend response shapes:
26
+ * 1. Not authenticated:
27
+ * { success: false, authenticated: false }
28
+ *
29
+ * 2. Authenticated:
30
+ * {
31
+ * success: true,
32
+ * authenticated: true,
33
+ * data: {
34
+ * userId,
35
+ * accessToken,
36
+ * expiresIn
37
+ * }
38
+ * }
39
+ */
40
+
41
+ if (!json.authenticated || !json.data?.accessToken) {
42
+ return { authenticated: false };
43
+ }
44
+
45
+ return {
46
+ authenticated: true,
47
+ accessToken: json.data.accessToken,
48
+ expiresAt: Date.now() + json.data.expiresIn * 1000,
49
+ };
50
+ }
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+ // import { AutherrSessionResponse } from "../types/auth";
59
+
60
+ // export async function fetchSession(
61
+ // baseUrl: string,
62
+ // clientId: string
63
+ // ): Promise<AutherrSessionResponse> {
64
+ // const res = await fetch(
65
+ // `${baseUrl}/__bff/auth/session?client_id=${clientId}`,
66
+ // {
67
+ // method: "GET",
68
+ // credentials: "include", // CRITICAL
69
+ // headers: {
70
+ // "Content-Type": "application/json",
71
+ // },
72
+ // }
73
+ // );
74
+
75
+ // if (!res.ok) {
76
+ // return { authenticated: false };
77
+ // }
78
+
79
+ // const data = (await res.json()) as AutherrSessionResponse;
80
+
81
+ // if (!data || !data.accessToken) {
82
+ // return { authenticated: false };
83
+ // }
84
+
85
+ // return {
86
+ // authenticated: true,
87
+ // user: data.user,
88
+ // accessToken: data.accessToken,
89
+ // expiresAt: data.expiresAt,
90
+ // };
91
+ // }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { useAutherr } from "../hooks/useAutherr";
3
+
4
+ interface LoginButtonProps {
5
+ children?: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function LoginButton({
10
+ children = "Sign in",
11
+ className,
12
+ }: LoginButtonProps) {
13
+ const { login, isAuthenticated } = useAutherr();
14
+
15
+ if (isAuthenticated) return null;
16
+
17
+ return (
18
+ <button
19
+ type="button"
20
+ onClick={login}
21
+ className={className}
22
+ >
23
+ {children}
24
+ </button>
25
+ );
26
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { useAutherr } from "../hooks/useAutherr";
3
+
4
+ interface SignupButtonProps {
5
+ children?: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function SignupButton({
10
+ children = "Sign up",
11
+ className,
12
+ }: SignupButtonProps) {
13
+ const { signup, isAuthenticated } = useAutherr();
14
+
15
+ if (isAuthenticated) return null;
16
+
17
+ return (
18
+ <button
19
+ type="button"
20
+ onClick={signup}
21
+ className={className}
22
+ >
23
+ {children}
24
+ </button>
25
+ );
26
+ }
@@ -0,0 +1,158 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { useAutherr } from "../hooks/useAutherr";
3
+
4
+ // simple local decoder (no verification)
5
+ function decodeJwt(token: string) {
6
+ try {
7
+ const [, payload] = token.split(".");
8
+ return JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ export function UserMenu() {
15
+ const {
16
+ isAuthenticated,
17
+ accessToken,
18
+ login,
19
+ signup,
20
+ logout,
21
+ } = useAutherr();
22
+
23
+ const [open, setOpen] = useState(false);
24
+ const [showPayload, setShowPayload] = useState(false);
25
+ const ref = useRef<HTMLDivElement>(null);
26
+
27
+ // close on outside click
28
+ useEffect(() => {
29
+ function handleClick(e: MouseEvent) {
30
+ if (ref.current && !ref.current.contains(e.target as Node)) {
31
+ setOpen(false);
32
+ setShowPayload(false);
33
+ }
34
+ }
35
+ document.addEventListener("mousedown", handleClick);
36
+ return () => document.removeEventListener("mousedown", handleClick);
37
+ }, []);
38
+
39
+ const payload = accessToken ? decodeJwt(accessToken) : null;
40
+ const initial = payload?.email?.[0]?.toUpperCase() ?? "U";
41
+
42
+ return (
43
+ <div ref={ref} style={{ position: "relative" }}>
44
+ {/* Avatar Button */}
45
+ <button
46
+ onClick={() => setOpen((v) => !v)}
47
+ style={{
48
+ width: 36,
49
+ height: 36,
50
+ borderRadius: "50%",
51
+ background: "#ff6a00",
52
+ color: "#fff",
53
+ border: "none",
54
+ cursor: "pointer",
55
+ fontWeight: 600,
56
+ }}
57
+ >
58
+ {isAuthenticated ? initial : "?"}
59
+ </button>
60
+
61
+ {/* Dropdown */}
62
+ {open && (
63
+ <div
64
+ style={{
65
+ position: "absolute",
66
+ top: "calc(100% + 8px)",
67
+ right: 0,
68
+ width: 280,
69
+ background: "#fff",
70
+ borderRadius: 12,
71
+ boxShadow: "0 10px 30px rgba(0,0,0,0.15)",
72
+ padding: 12,
73
+ zIndex: 1000,
74
+ }}
75
+ >
76
+ {isAuthenticated ? (
77
+ <>
78
+ {/* Header */}
79
+ <div style={{ marginBottom: 12 }}>
80
+ <div style={{ fontWeight: 600 }}>{payload?.email}</div>
81
+ <div style={{ fontSize: 12, color: "#666" }}>
82
+ Signed in
83
+ </div>
84
+ </div>
85
+
86
+ <Divider />
87
+
88
+ <MenuItem onClick={() => setShowPayload((v) => !v)}>
89
+ {showPayload ? "Hide payload" : "View payload"}
90
+ </MenuItem>
91
+
92
+ <MenuItem onClick={logout}>Sign out</MenuItem>
93
+
94
+ {showPayload && payload && (
95
+ <pre
96
+ style={{
97
+ marginTop: 10,
98
+ background: "#0b0b0b",
99
+ color: "#00ff88",
100
+ padding: 10,
101
+ borderRadius: 8,
102
+ fontSize: 12,
103
+ maxHeight: 200,
104
+ overflow: "auto",
105
+ }}
106
+ >
107
+ {JSON.stringify(payload, null, 2)}
108
+ </pre>
109
+ )}
110
+ </>
111
+ ) : (
112
+ <>
113
+ <MenuItem onClick={login}>Sign in</MenuItem>
114
+ <MenuItem onClick={signup}>Sign up</MenuItem>
115
+ </>
116
+ )}
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ function MenuItem({
124
+ children,
125
+ onClick,
126
+ }: {
127
+ children: React.ReactNode;
128
+ onClick: () => void;
129
+ }) {
130
+ return (
131
+ <button
132
+ onClick={onClick}
133
+ style={{
134
+ width: "100%",
135
+ padding: "8px 0",
136
+ background: "none",
137
+ border: "none",
138
+ textAlign: "left",
139
+ cursor: "pointer",
140
+ fontSize: 14,
141
+ }}
142
+ >
143
+ {children}
144
+ </button>
145
+ );
146
+ }
147
+
148
+ function Divider() {
149
+ return (
150
+ <div
151
+ style={{
152
+ height: 1,
153
+ background: "#eee",
154
+ margin: "8px 0",
155
+ }}
156
+ />
157
+ );
158
+ }
@@ -0,0 +1,25 @@
1
+ import { useAutherrContext } from "../provider/AutherrProvider";
2
+
3
+ export function useAutherr() {
4
+ const {
5
+ user,
6
+ accessToken,
7
+ isAuthenticated,
8
+ login,
9
+ signup,
10
+ logout,
11
+ getAccessToken,
12
+ refreshSession,
13
+ } = useAutherrContext();
14
+
15
+ return {
16
+ user,
17
+ accessToken,
18
+ isAuthenticated,
19
+ login,
20
+ signup,
21
+ logout,
22
+ refreshSession,
23
+ getAccessToken,
24
+ };
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { AutherrProvider } from "./provider/AutherrProvider";
2
+ export { useAutherr } from "./hooks/useAutherr";
3
+
4
+ export { LoginButton } from "./components/LoginButton";
5
+ export { SignupButton } from "./components/SignupButton";
6
+ export { UserMenu } from "./components/UserMenu";
@@ -0,0 +1,114 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ } from "react";
8
+ import { fetchSession } from "../api/session";
9
+ import { logoutSession } from "../api/logout";
10
+ import { AutherrUser } from "../types/auth";
11
+
12
+ interface AutherrContextValue {
13
+ user: AutherrUser | null;
14
+ accessToken: string | null;
15
+ isAuthenticated: boolean;
16
+ login: () => void;
17
+ signup: () => void;
18
+ logout: () => Promise<void>;
19
+ getAccessToken: () => string | null;
20
+ refreshSession: () => Promise<void>;
21
+ }
22
+
23
+ const AutherrContext = createContext<AutherrContextValue | null>(null);
24
+
25
+ interface AutherrProviderProps {
26
+ children: React.ReactNode;
27
+ clientId: string;
28
+ baseUrl: string; // e.g. https://autherr.com
29
+ }
30
+
31
+ export function AutherrProvider({
32
+ children,
33
+ clientId,
34
+ baseUrl,
35
+ }: AutherrProviderProps) {
36
+ const [user, setUser] = useState<AutherrUser | null>(null);
37
+ const [accessToken, setAccessToken] = useState<string | null>(null);
38
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
39
+
40
+ const refreshSession = async () => {
41
+ const session = await fetchSession(baseUrl, clientId);
42
+
43
+ if (session.authenticated && session.accessToken) {
44
+ setAccessToken(session.accessToken);
45
+ setIsAuthenticated(true);
46
+ } else {
47
+ setUser(null);
48
+ setAccessToken(null);
49
+ setIsAuthenticated(false);
50
+ }
51
+ };
52
+
53
+
54
+ useEffect(() => {
55
+ refreshSession();
56
+ }, [baseUrl, clientId]);
57
+
58
+ const login = () => {
59
+ const state = crypto.randomUUID();
60
+ window.location.href =
61
+ `${baseUrl}/auth/login` +
62
+ `?client_id=${clientId}` +
63
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}` +
64
+ `&state=${state}`;
65
+ };
66
+
67
+ const signup = () => {
68
+ const state = crypto.randomUUID();
69
+ window.location.href =
70
+ `${baseUrl}/auth/signup` +
71
+ `?client_id=${clientId}` +
72
+ `&redirect_uri=${encodeURIComponent(window.location.origin)}` +
73
+ `&state=${state}`;
74
+ };
75
+
76
+ const logout = async () => {
77
+ try {
78
+ await logoutSession(baseUrl, clientId);
79
+ } finally {
80
+ setUser(null);
81
+ setAccessToken(null);
82
+ setIsAuthenticated(false);
83
+ window.location.href = window.location.origin;
84
+ }
85
+ };
86
+
87
+ const value = useMemo(
88
+ () => ({
89
+ user,
90
+ accessToken,
91
+ isAuthenticated,
92
+ login,
93
+ signup,
94
+ logout,
95
+ refreshSession,
96
+ getAccessToken: () => accessToken,
97
+ }),
98
+ [user, accessToken, isAuthenticated]
99
+ );
100
+
101
+ return (
102
+ <AutherrContext.Provider value={value}>
103
+ {children}
104
+ </AutherrContext.Provider>
105
+ );
106
+ }
107
+
108
+ export function useAutherrContext() {
109
+ const ctx = useContext(AutherrContext);
110
+ if (!ctx) {
111
+ throw new Error("useAutherrContext must be used inside AutherrProvider");
112
+ }
113
+ return ctx;
114
+ }
@@ -0,0 +1,13 @@
1
+ export interface AutherrUser {
2
+ sub: string;
3
+ email: string;
4
+ userType: "platform" | "application";
5
+ applicationId?: string;
6
+ }
7
+
8
+ export interface AutherrSessionResponse {
9
+ authenticated: boolean;
10
+ user?: AutherrUser;
11
+ accessToken?: string;
12
+ expiresAt?: number;
13
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "jsx": "react-jsx",
7
+ "declaration": true,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true
14
+ },
15
+ "include": ["src"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }