@arcote.tech/arc-auth 0.4.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 +20 -0
- package/src/aggregates/account.ts +301 -0
- package/src/aggregates/oauth-identity.ts +131 -0
- package/src/arc.d.ts +14 -0
- package/src/auth-builder.ts +132 -0
- package/src/ids/account.ts +13 -0
- package/src/ids/oauth-identity.ts +15 -0
- package/src/index.ts +125 -0
- package/src/providers/facebook.ts +82 -0
- package/src/providers/google.ts +113 -0
- package/src/providers/index.ts +10 -0
- package/src/providers/types.ts +53 -0
- package/src/react/auth-page.tsx +258 -0
- package/src/react/auth-provider.tsx +82 -0
- package/src/react/index.ts +11 -0
- package/src/react/protected-route.tsx +60 -0
- package/src/react/sign-in-page.tsx +161 -0
- package/src/routes/oauth-routes.ts +276 -0
- package/src/tokens/token.ts +15 -0
- package/tsconfig.json +4 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aggregateContextElement,
|
|
3
|
+
context,
|
|
4
|
+
type ArcRawShape,
|
|
5
|
+
} from "@arcote.tech/arc";
|
|
6
|
+
import { createAccountAggregate } from "./aggregates/account";
|
|
7
|
+
import { createOAuthIdentityAggregate } from "./aggregates/oauth-identity";
|
|
8
|
+
import { createAccountId } from "./ids/account";
|
|
9
|
+
import { createOAuthIdentityId } from "./ids/oauth-identity";
|
|
10
|
+
import { createToken } from "./tokens/token";
|
|
11
|
+
import { createOAuthRoutes } from "./routes/oauth-routes";
|
|
12
|
+
import type { OAuthProvidersConfig } from "./providers/types";
|
|
13
|
+
|
|
14
|
+
// --- New typed builder API ---
|
|
15
|
+
export { auth, AuthBuilder } from "./auth-builder";
|
|
16
|
+
|
|
17
|
+
// --- Deprecated: use auth().useOAuth().build() instead ---
|
|
18
|
+
|
|
19
|
+
export type AuthMode = "email" | "oauth" | "both";
|
|
20
|
+
|
|
21
|
+
export type AuthContextData = {
|
|
22
|
+
name: string;
|
|
23
|
+
customFields: ArcRawShape;
|
|
24
|
+
secret: string | undefined;
|
|
25
|
+
mode?: AuthMode;
|
|
26
|
+
providers?: OAuthProvidersConfig;
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** @deprecated Use `auth({...}).useOAuth({...}).build()` instead */
|
|
31
|
+
export const createAuthContext = <const Data extends AuthContextData>(
|
|
32
|
+
data: Data,
|
|
33
|
+
) => {
|
|
34
|
+
const mode = data.mode ?? "both";
|
|
35
|
+
const accountId = createAccountId({ name: data.name });
|
|
36
|
+
const token = createToken({ name: data.name, secret: data.secret });
|
|
37
|
+
|
|
38
|
+
const Account = createAccountAggregate({
|
|
39
|
+
name: data.name,
|
|
40
|
+
accountId,
|
|
41
|
+
token,
|
|
42
|
+
customFields: data.customFields,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const accountElement = aggregateContextElement(Account);
|
|
46
|
+
|
|
47
|
+
const elements: any[] = [accountElement];
|
|
48
|
+
|
|
49
|
+
let OAuthIdentity: ReturnType<typeof createOAuthIdentityAggregate> | undefined;
|
|
50
|
+
let enabledProviders: string[] = [];
|
|
51
|
+
|
|
52
|
+
if (mode !== "email" && data.providers) {
|
|
53
|
+
const oauthIdentityId = createOAuthIdentityId({ name: data.name });
|
|
54
|
+
|
|
55
|
+
OAuthIdentity = createOAuthIdentityAggregate({
|
|
56
|
+
name: data.name,
|
|
57
|
+
oauthIdentityId,
|
|
58
|
+
accountId,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const oauthIdentityElement = aggregateContextElement(OAuthIdentity);
|
|
62
|
+
elements.push(oauthIdentityElement);
|
|
63
|
+
|
|
64
|
+
if (data.baseUrl) {
|
|
65
|
+
const oauthRoutes = createOAuthRoutes({
|
|
66
|
+
providers: data.providers,
|
|
67
|
+
baseUrl: data.baseUrl,
|
|
68
|
+
token,
|
|
69
|
+
accountElement,
|
|
70
|
+
oauthIdentityElement,
|
|
71
|
+
});
|
|
72
|
+
elements.push(oauthRoutes.oauthStart);
|
|
73
|
+
elements.push(oauthRoutes.oauthCallback);
|
|
74
|
+
enabledProviders = oauthRoutes.enabledProviders;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const authContext = context(elements);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
context: authContext,
|
|
82
|
+
accountId,
|
|
83
|
+
token,
|
|
84
|
+
Account,
|
|
85
|
+
OAuthIdentity,
|
|
86
|
+
mode,
|
|
87
|
+
enabledProviders,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type AuthContext<Data extends AuthContextData = AuthContextData> =
|
|
92
|
+
ReturnType<typeof createAuthContext<Data>>;
|
|
93
|
+
|
|
94
|
+
// Re-exports
|
|
95
|
+
export { createAccountAggregate } from "./aggregates/account";
|
|
96
|
+
export type { AccountAggregate } from "./aggregates/account";
|
|
97
|
+
export { createOAuthIdentityAggregate } from "./aggregates/oauth-identity";
|
|
98
|
+
export type { OAuthIdentityAggregate } from "./aggregates/oauth-identity";
|
|
99
|
+
export { createAccountId } from "./ids/account";
|
|
100
|
+
export type { AccountId } from "./ids/account";
|
|
101
|
+
export { createOAuthIdentityId } from "./ids/oauth-identity";
|
|
102
|
+
export type { OAuthIdentityId } from "./ids/oauth-identity";
|
|
103
|
+
export { createToken } from "./tokens/token";
|
|
104
|
+
export type { Token } from "./tokens/token";
|
|
105
|
+
|
|
106
|
+
// Provider types
|
|
107
|
+
export type {
|
|
108
|
+
OAuthProviderConfig,
|
|
109
|
+
OAuthProvidersConfig,
|
|
110
|
+
OAuthProviderName,
|
|
111
|
+
} from "./providers/types";
|
|
112
|
+
|
|
113
|
+
// React components
|
|
114
|
+
export { AuthProvider, ProtectedRoute, SignInPage, useAuth } from "./react";
|
|
115
|
+
export type {
|
|
116
|
+
AuthContextType,
|
|
117
|
+
AuthProviderProps,
|
|
118
|
+
ProtectedRouteProps,
|
|
119
|
+
SignInPageProps,
|
|
120
|
+
SignInRenderProps,
|
|
121
|
+
} from "./react";
|
|
122
|
+
|
|
123
|
+
// New auth page
|
|
124
|
+
export { AuthPage } from "./react/auth-page";
|
|
125
|
+
export type { AuthPageProps, AuthPageRenderProps } from "./react/auth-page";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OAuthProviderAdapter,
|
|
3
|
+
OAuthTokenResponse,
|
|
4
|
+
OAuthUserProfile,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
const FB_AUTH_URL = "https://www.facebook.com/v21.0/dialog/oauth";
|
|
8
|
+
const FB_TOKEN_URL = "https://graph.facebook.com/v21.0/oauth/access_token";
|
|
9
|
+
const FB_USERINFO_URL = "https://graph.facebook.com/me";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_FACEBOOK_SCOPES = ["email", "public_profile"];
|
|
12
|
+
|
|
13
|
+
export const facebookProvider: OAuthProviderAdapter = {
|
|
14
|
+
name: "facebook",
|
|
15
|
+
|
|
16
|
+
buildAuthorizationUrl({ clientId, redirectUri, state, scopes }) {
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
client_id: clientId,
|
|
19
|
+
redirect_uri: redirectUri,
|
|
20
|
+
scope: scopes.join(","),
|
|
21
|
+
state,
|
|
22
|
+
response_type: "code",
|
|
23
|
+
});
|
|
24
|
+
return `${FB_AUTH_URL}?${params.toString()}`;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async exchangeCode({
|
|
28
|
+
code,
|
|
29
|
+
clientId,
|
|
30
|
+
clientSecret,
|
|
31
|
+
redirectUri,
|
|
32
|
+
}): Promise<OAuthTokenResponse> {
|
|
33
|
+
const params = new URLSearchParams({
|
|
34
|
+
code,
|
|
35
|
+
client_id: clientId,
|
|
36
|
+
client_secret: clientSecret,
|
|
37
|
+
redirect_uri: redirectUri,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const response = await fetch(`${FB_TOKEN_URL}?${params.toString()}`);
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Facebook token exchange failed: ${response.status} ${text}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
return {
|
|
51
|
+
accessToken: data.access_token,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async fetchUserProfile(accessToken: string): Promise<OAuthUserProfile> {
|
|
56
|
+
const params = new URLSearchParams({
|
|
57
|
+
fields: "id,name,email,picture.width(200).height(200)",
|
|
58
|
+
access_token: accessToken,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const response = await fetch(`${FB_USERINFO_URL}?${params.toString()}`);
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`Facebook userinfo fetch failed: ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
|
|
69
|
+
if (!data.email) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Facebook did not return an email. User may not have an email associated with their account.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
providerUserId: data.id,
|
|
77
|
+
email: data.email,
|
|
78
|
+
displayName: data.name,
|
|
79
|
+
avatarUrl: data.picture?.data?.url,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OAuthProviderAdapter,
|
|
3
|
+
OAuthTokenResponse,
|
|
4
|
+
OAuthUserProfile,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8
|
+
const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
9
|
+
const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_GOOGLE_SCOPES = ["openid", "email", "profile"];
|
|
12
|
+
|
|
13
|
+
export const googleProvider: OAuthProviderAdapter = {
|
|
14
|
+
name: "google",
|
|
15
|
+
|
|
16
|
+
buildAuthorizationUrl({ clientId, redirectUri, state, scopes }) {
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
client_id: clientId,
|
|
19
|
+
redirect_uri: redirectUri,
|
|
20
|
+
response_type: "code",
|
|
21
|
+
scope: scopes.join(" "),
|
|
22
|
+
state,
|
|
23
|
+
access_type: "offline",
|
|
24
|
+
prompt: "consent",
|
|
25
|
+
});
|
|
26
|
+
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async exchangeCode({
|
|
30
|
+
code,
|
|
31
|
+
clientId,
|
|
32
|
+
clientSecret,
|
|
33
|
+
redirectUri,
|
|
34
|
+
}): Promise<OAuthTokenResponse> {
|
|
35
|
+
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
38
|
+
body: new URLSearchParams({
|
|
39
|
+
code,
|
|
40
|
+
client_id: clientId,
|
|
41
|
+
client_secret: clientSecret,
|
|
42
|
+
redirect_uri: redirectUri,
|
|
43
|
+
grant_type: "authorization_code",
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
throw new Error(`Google token exchange failed: ${response.status} ${text}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
return {
|
|
54
|
+
accessToken: data.access_token,
|
|
55
|
+
refreshToken: data.refresh_token,
|
|
56
|
+
idToken: data.id_token,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async fetchUserProfile(
|
|
61
|
+
accessToken: string,
|
|
62
|
+
idToken?: string,
|
|
63
|
+
): Promise<OAuthUserProfile> {
|
|
64
|
+
// Try decoding id_token first (avoids extra network call)
|
|
65
|
+
if (idToken) {
|
|
66
|
+
const profile = decodeIdToken(idToken);
|
|
67
|
+
if (profile) return profile;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Fallback: call userinfo endpoint
|
|
71
|
+
const response = await fetch(GOOGLE_USERINFO_URL, {
|
|
72
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Google userinfo fetch failed: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
return {
|
|
81
|
+
providerUserId: data.id,
|
|
82
|
+
email: data.email,
|
|
83
|
+
displayName: data.name,
|
|
84
|
+
avatarUrl: data.picture,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decode Google id_token JWT payload (no signature verification needed —
|
|
91
|
+
* we already verified the token exchange with Google's server).
|
|
92
|
+
*/
|
|
93
|
+
function decodeIdToken(idToken: string): OAuthUserProfile | null {
|
|
94
|
+
try {
|
|
95
|
+
const parts = idToken.split(".");
|
|
96
|
+
if (parts.length !== 3) return null;
|
|
97
|
+
|
|
98
|
+
const payload = JSON.parse(
|
|
99
|
+
Buffer.from(parts[1], "base64url").toString("utf-8"),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!payload.sub || !payload.email) return null;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
providerUserId: payload.sub,
|
|
106
|
+
email: payload.email,
|
|
107
|
+
displayName: payload.name,
|
|
108
|
+
avatarUrl: payload.picture,
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { googleProvider, DEFAULT_GOOGLE_SCOPES } from "./google";
|
|
2
|
+
export { facebookProvider, DEFAULT_FACEBOOK_SCOPES } from "./facebook";
|
|
3
|
+
export type {
|
|
4
|
+
OAuthProviderAdapter,
|
|
5
|
+
OAuthTokenResponse,
|
|
6
|
+
OAuthUserProfile,
|
|
7
|
+
OAuthProviderName,
|
|
8
|
+
OAuthProviderConfig,
|
|
9
|
+
OAuthProvidersConfig,
|
|
10
|
+
} from "./types";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth provider adapter interface.
|
|
3
|
+
* Each provider (Google, Facebook) implements this to handle
|
|
4
|
+
* authorization URL building, code exchange, and profile fetching.
|
|
5
|
+
*/
|
|
6
|
+
export interface OAuthProviderAdapter {
|
|
7
|
+
name: "google" | "facebook";
|
|
8
|
+
|
|
9
|
+
buildAuthorizationUrl(params: {
|
|
10
|
+
clientId: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
state: string;
|
|
13
|
+
scopes: string[];
|
|
14
|
+
}): string;
|
|
15
|
+
|
|
16
|
+
exchangeCode(params: {
|
|
17
|
+
code: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
redirectUri: string;
|
|
21
|
+
}): Promise<OAuthTokenResponse>;
|
|
22
|
+
|
|
23
|
+
fetchUserProfile(
|
|
24
|
+
accessToken: string,
|
|
25
|
+
idToken?: string,
|
|
26
|
+
): Promise<OAuthUserProfile>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OAuthTokenResponse {
|
|
30
|
+
accessToken: string;
|
|
31
|
+
refreshToken?: string;
|
|
32
|
+
idToken?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OAuthUserProfile {
|
|
36
|
+
providerUserId: string;
|
|
37
|
+
email: string;
|
|
38
|
+
displayName?: string;
|
|
39
|
+
avatarUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type OAuthProviderName = "google" | "facebook";
|
|
43
|
+
|
|
44
|
+
export type OAuthProviderConfig = {
|
|
45
|
+
clientId: string;
|
|
46
|
+
clientSecret: string;
|
|
47
|
+
scopes?: string[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type OAuthProvidersConfig = {
|
|
51
|
+
google?: OAuthProviderConfig;
|
|
52
|
+
facebook?: OAuthProviderConfig;
|
|
53
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Trans } from "@arcote.tech/platform";
|
|
2
|
+
import { useState, useCallback, type ReactNode } from "react";
|
|
3
|
+
import { useAuth } from "./auth-provider";
|
|
4
|
+
|
|
5
|
+
type AuthMode = "email" | "oauth" | "both";
|
|
6
|
+
|
|
7
|
+
export interface AuthPageProps {
|
|
8
|
+
mode: AuthMode;
|
|
9
|
+
enabledProviders?: Array<"google" | "facebook">;
|
|
10
|
+
/** signIn command — required when mode is "email" or "both" */
|
|
11
|
+
signIn?: (params: { email: string; password: string }) => Promise<any>;
|
|
12
|
+
navigate: (path: string) => void;
|
|
13
|
+
/** Base URL for OAuth start links (e.g. "/api" or "") */
|
|
14
|
+
apiBaseUrl?: string;
|
|
15
|
+
/** Custom render — receives form state and handlers */
|
|
16
|
+
render?: (props: AuthPageRenderProps) => ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AuthPageRenderProps {
|
|
20
|
+
// Email/password fields (present only when mode != "oauth")
|
|
21
|
+
email?: string;
|
|
22
|
+
setEmail?: (v: string) => void;
|
|
23
|
+
password?: string;
|
|
24
|
+
setPassword?: (v: string) => void;
|
|
25
|
+
handleEmailSubmit?: (e: { preventDefault: () => void }) => void;
|
|
26
|
+
|
|
27
|
+
// OAuth
|
|
28
|
+
enabledProviders: Array<"google" | "facebook">;
|
|
29
|
+
getOAuthUrl: (provider: string) => string;
|
|
30
|
+
|
|
31
|
+
// Shared
|
|
32
|
+
error: string;
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
mode: AuthMode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function AuthPage({
|
|
38
|
+
mode,
|
|
39
|
+
enabledProviders = [],
|
|
40
|
+
signIn,
|
|
41
|
+
navigate,
|
|
42
|
+
apiBaseUrl = "",
|
|
43
|
+
render,
|
|
44
|
+
}: AuthPageProps) {
|
|
45
|
+
const { setAccessToken } = useAuth();
|
|
46
|
+
|
|
47
|
+
const [email, setEmail] = useState("");
|
|
48
|
+
const [password, setPassword] = useState("");
|
|
49
|
+
const [error, setError] = useState("");
|
|
50
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
51
|
+
|
|
52
|
+
const redirectTo =
|
|
53
|
+
new URLSearchParams(window.location.search).get("redirectTo") || "/";
|
|
54
|
+
|
|
55
|
+
const getOAuthUrl = useCallback(
|
|
56
|
+
(provider: string) => {
|
|
57
|
+
return `${apiBaseUrl}/route/auth/oauth/${provider}/start?redirectTo=${encodeURIComponent(redirectTo)}`;
|
|
58
|
+
},
|
|
59
|
+
[apiBaseUrl, redirectTo],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handleEmailSubmit = async (e: { preventDefault: () => void }) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
if (!signIn) return;
|
|
65
|
+
|
|
66
|
+
setError("");
|
|
67
|
+
setIsLoading(true);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await signIn({ email, password });
|
|
71
|
+
|
|
72
|
+
if (result && "error" in result) {
|
|
73
|
+
if (result.error === "INVALID_EMAIL_OR_PASSWORD") {
|
|
74
|
+
setError("Nieprawidłowy email lub hasło.");
|
|
75
|
+
} else if (result.error === "EMAIL_NOT_VERIFIED") {
|
|
76
|
+
setError("Email nie został zweryfikowany.");
|
|
77
|
+
} else {
|
|
78
|
+
setError("Wystąpił błąd podczas logowania.");
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setAccessToken(result.token);
|
|
84
|
+
setTimeout(() => navigate(redirectTo), 50);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("[AuthPage] signIn error:", err);
|
|
87
|
+
setError("Nie udało się połączyć z serwerem.");
|
|
88
|
+
} finally {
|
|
89
|
+
setIsLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Check for auth_error from OAuth callback
|
|
94
|
+
const authError = new URLSearchParams(window.location.search).get("auth_error");
|
|
95
|
+
const displayError = authError
|
|
96
|
+
? `Błąd logowania OAuth: ${authError}`
|
|
97
|
+
: error;
|
|
98
|
+
|
|
99
|
+
const includeEmail = mode === "email" || mode === "both";
|
|
100
|
+
|
|
101
|
+
const renderProps: AuthPageRenderProps = {
|
|
102
|
+
...(includeEmail
|
|
103
|
+
? {
|
|
104
|
+
email,
|
|
105
|
+
setEmail,
|
|
106
|
+
password,
|
|
107
|
+
setPassword,
|
|
108
|
+
handleEmailSubmit,
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
enabledProviders,
|
|
112
|
+
getOAuthUrl,
|
|
113
|
+
error: displayError,
|
|
114
|
+
isLoading,
|
|
115
|
+
mode,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (render) return <>{render(renderProps)}</>;
|
|
119
|
+
|
|
120
|
+
// Default minimal form
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
style={{
|
|
124
|
+
display: "flex",
|
|
125
|
+
minHeight: "100vh",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
justifyContent: "center",
|
|
128
|
+
padding: 16,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<div
|
|
132
|
+
style={{
|
|
133
|
+
width: "100%",
|
|
134
|
+
maxWidth: 360,
|
|
135
|
+
display: "flex",
|
|
136
|
+
flexDirection: "column",
|
|
137
|
+
gap: 16,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<h1 style={{ fontSize: 24, textAlign: "center" }}><Trans>Zaloguj się</Trans></h1>
|
|
141
|
+
|
|
142
|
+
{displayError && (
|
|
143
|
+
<div
|
|
144
|
+
style={{
|
|
145
|
+
padding: 12,
|
|
146
|
+
background: "#fef2f2",
|
|
147
|
+
border: "1px solid #fecaca",
|
|
148
|
+
borderRadius: 6,
|
|
149
|
+
color: "#dc2626",
|
|
150
|
+
fontSize: 14,
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{displayError}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* OAuth buttons */}
|
|
158
|
+
{enabledProviders.length > 0 && (
|
|
159
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
160
|
+
{enabledProviders.map((provider) => (
|
|
161
|
+
<a
|
|
162
|
+
key={provider}
|
|
163
|
+
href={getOAuthUrl(provider)}
|
|
164
|
+
style={{
|
|
165
|
+
display: "flex",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
justifyContent: "center",
|
|
168
|
+
gap: 8,
|
|
169
|
+
padding: "10px 16px",
|
|
170
|
+
border: "1px solid #d1d5db",
|
|
171
|
+
borderRadius: 6,
|
|
172
|
+
textDecoration: "none",
|
|
173
|
+
color: "#374151",
|
|
174
|
+
fontSize: 14,
|
|
175
|
+
cursor: "pointer",
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
Zaloguj się przez {provider === "google" ? "Google" : "Facebook"}
|
|
179
|
+
</a>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{/* Separator */}
|
|
185
|
+
{includeEmail && enabledProviders.length > 0 && (
|
|
186
|
+
<div
|
|
187
|
+
style={{
|
|
188
|
+
display: "flex",
|
|
189
|
+
alignItems: "center",
|
|
190
|
+
gap: 12,
|
|
191
|
+
color: "#9ca3af",
|
|
192
|
+
fontSize: 12,
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<div style={{ flex: 1, height: 1, background: "#e5e7eb" }} />
|
|
196
|
+
<Trans>lub</Trans>
|
|
197
|
+
<div style={{ flex: 1, height: 1, background: "#e5e7eb" }} />
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Email/password form */}
|
|
202
|
+
{includeEmail && (
|
|
203
|
+
<form
|
|
204
|
+
onSubmit={handleEmailSubmit}
|
|
205
|
+
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
|
206
|
+
>
|
|
207
|
+
<input
|
|
208
|
+
type="email"
|
|
209
|
+
placeholder="Email"
|
|
210
|
+
value={email}
|
|
211
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
212
|
+
required
|
|
213
|
+
disabled={isLoading}
|
|
214
|
+
autoComplete="email"
|
|
215
|
+
style={{
|
|
216
|
+
padding: 8,
|
|
217
|
+
border: "1px solid #d1d5db",
|
|
218
|
+
borderRadius: 6,
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
<input
|
|
223
|
+
type="password"
|
|
224
|
+
placeholder="Hasło"
|
|
225
|
+
value={password}
|
|
226
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
227
|
+
required
|
|
228
|
+
disabled={isLoading}
|
|
229
|
+
minLength={6}
|
|
230
|
+
maxLength={32}
|
|
231
|
+
autoComplete="current-password"
|
|
232
|
+
style={{
|
|
233
|
+
padding: 8,
|
|
234
|
+
border: "1px solid #d1d5db",
|
|
235
|
+
borderRadius: 6,
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
|
|
239
|
+
<button
|
|
240
|
+
type="submit"
|
|
241
|
+
disabled={isLoading}
|
|
242
|
+
style={{
|
|
243
|
+
padding: "8px 16px",
|
|
244
|
+
background: "#2563eb",
|
|
245
|
+
color: "white",
|
|
246
|
+
border: "none",
|
|
247
|
+
borderRadius: 6,
|
|
248
|
+
cursor: "pointer",
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{isLoading ? "Logowanie..." : "Zaloguj się"}
|
|
252
|
+
</button>
|
|
253
|
+
</form>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|