@stackframe/stack 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/LICENSE +7 -0
- package/dist/components/EmailVerification.d.ts +5 -0
- package/dist/components/EmailVerification.js +29 -0
- package/dist/components/ForgotPassword.d.ts +3 -0
- package/dist/components/ForgotPassword.js +22 -0
- package/dist/components/OauthCallback.d.ts +1 -0
- package/dist/components/OauthCallback.js +20 -0
- package/dist/components/PasswordReset.d.ts +4 -0
- package/dist/components/PasswordReset.js +36 -0
- package/dist/components/SignIn.d.ts +4 -0
- package/dist/components/SignIn.js +18 -0
- package/dist/components/SignOut.d.ts +3 -0
- package/dist/components/SignOut.js +12 -0
- package/dist/components/SignUp.d.ts +4 -0
- package/dist/components/SignUp.js +18 -0
- package/dist/components/StackHandler.d.ts +8 -0
- package/dist/components/StackHandler.js +58 -0
- package/dist/elements/Button.d.ts +10 -0
- package/dist/elements/Button.js +18 -0
- package/dist/elements/CardFrame.d.ts +5 -0
- package/dist/elements/CardFrame.js +10 -0
- package/dist/elements/CardHeader.d.ts +5 -0
- package/dist/elements/CardHeader.js +4 -0
- package/dist/elements/CredentialSignIn.d.ts +3 -0
- package/dist/elements/CredentialSignIn.js +57 -0
- package/dist/elements/CredentialSignUp.d.ts +3 -0
- package/dist/elements/CredentialSignUp.js +73 -0
- package/dist/elements/DividerWithText.d.ts +3 -0
- package/dist/elements/DividerWithText.js +5 -0
- package/dist/elements/ForgotPassword.d.ts +3 -0
- package/dist/elements/ForgotPassword.js +32 -0
- package/dist/elements/FormWarning.d.ts +3 -0
- package/dist/elements/FormWarning.js +7 -0
- package/dist/elements/MessageCard.d.ts +6 -0
- package/dist/elements/MessageCard.js +5 -0
- package/dist/elements/OauthButton.d.ts +5 -0
- package/dist/elements/OauthButton.js +67 -0
- package/dist/elements/OauthGroup.d.ts +4 -0
- package/dist/elements/OauthGroup.js +11 -0
- package/dist/elements/PasswordField.d.ts +2 -0
- package/dist/elements/PasswordField.js +27 -0
- package/dist/elements/PasswordResetInner.d.ts +4 -0
- package/dist/elements/PasswordResetInner.js +61 -0
- package/dist/elements/RedirectMessageCard.d.ts +4 -0
- package/dist/elements/RedirectMessageCard.js +49 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/lib/auth.d.ts +19 -0
- package/dist/lib/auth.js +85 -0
- package/dist/lib/cookie.d.ts +12 -0
- package/dist/lib/cookie.js +60 -0
- package/dist/lib/hooks.d.ts +21 -0
- package/dist/lib/hooks.js +21 -0
- package/dist/lib/stack-app.d.ts +172 -0
- package/dist/lib/stack-app.js +575 -0
- package/dist/providers/StackProvider.d.ts +6 -0
- package/dist/providers/StackProvider.js +6 -0
- package/dist/providers/StackProviderClient.d.ts +9 -0
- package/dist/providers/StackProviderClient.js +14 -0
- package/dist/tailwind.css +1293 -0
- package/dist/utils/email.d.ts +1 -0
- package/dist/utils/email.js +7 -0
- package/dist/utils/next.d.ts +1 -0
- package/dist/utils/next.js +4 -0
- package/dist/utils/react.d.ts +1 -0
- package/dist/utils/react.js +6 -0
- package/dist/utils/results.d.ts +24 -0
- package/dist/utils/results.js +46 -0
- package/dist/utils/types.d.ts +3 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.js +16 -0
- package/package.json +50 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { FaGithub, FaFacebook, FaApple } from 'react-icons/fa';
|
|
4
|
+
import { useStackApp } from '..';
|
|
5
|
+
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
|
6
|
+
import Button from './Button';
|
|
7
|
+
const iconSize = 24;
|
|
8
|
+
export default function OauthButton({ provider, type, redirectUrl }) {
|
|
9
|
+
const stackApp = useStackApp();
|
|
10
|
+
let style;
|
|
11
|
+
switch (provider) {
|
|
12
|
+
case 'google': {
|
|
13
|
+
style = {
|
|
14
|
+
backgroundColor: '#fff',
|
|
15
|
+
textColor: '#000',
|
|
16
|
+
name: 'Google',
|
|
17
|
+
icon: (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: iconSize, height: iconSize, viewBox: "0 0 24 24", children: [_jsx("path", { fill: "#4285F4", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "#34A853", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "#FBBC05", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "#EA4335", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" }), _jsx("path", { fill: "none", d: "M1 1h22v22H1z" })] })),
|
|
18
|
+
};
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'github': {
|
|
22
|
+
style = {
|
|
23
|
+
backgroundColor: '#111',
|
|
24
|
+
textColor: '#fff',
|
|
25
|
+
name: 'GitHub',
|
|
26
|
+
icon: (_jsx(FaGithub, { color: "#fff", size: iconSize })),
|
|
27
|
+
};
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'facebook': {
|
|
31
|
+
style = {
|
|
32
|
+
backgroundColor: '#1877F2',
|
|
33
|
+
textColor: '#fff',
|
|
34
|
+
name: 'Facebook',
|
|
35
|
+
icon: (_jsx(FaFacebook, { color: "#fff", size: iconSize })),
|
|
36
|
+
};
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case 'apple': {
|
|
40
|
+
style = {
|
|
41
|
+
backgroundColor: '#000',
|
|
42
|
+
textColor: '#fff',
|
|
43
|
+
name: 'Apple',
|
|
44
|
+
icon: (_jsx(FaApple, { color: "#fff", size: iconSize })),
|
|
45
|
+
};
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case 'microsoft': {
|
|
49
|
+
style = {
|
|
50
|
+
backgroundColor: '#2f2f2f',
|
|
51
|
+
textColor: '#fff',
|
|
52
|
+
name: 'Microsoft',
|
|
53
|
+
icon: (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: iconSize, height: iconSize, viewBox: "0 0 21 21", children: [_jsx("title", { children: "MS-SymbolLockup" }), _jsx("path", { fill: "#f25022", d: "M1 1h9v9H1z" }), _jsx("path", { fill: "#00a4ef", d: "M1 11h9v9H1z" }), _jsx("path", { fill: "#7fba00", d: "M11 1h9v9h-9z" }), _jsx("path", { fill: "#ffb900", d: "M11 11h9v9h-9z" })] })),
|
|
54
|
+
};
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
default: {
|
|
58
|
+
style = {
|
|
59
|
+
backgroundColor: '#000',
|
|
60
|
+
textColor: '#fff',
|
|
61
|
+
name: provider,
|
|
62
|
+
icon: null
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return (_jsxs(Button, { style: { backgroundColor: style.backgroundColor, color: style.textColor }, onClick: () => runAsynchronously(stackApp.signInWithOauth(provider)), leftIcon: style.icon, children: [type === 'signup' ? 'Sign up with ' : 'Sign in with ', style.name] }));
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useStackApp } from "..";
|
|
3
|
+
import OauthButton from "./OauthButton";
|
|
4
|
+
export default function OauthGroup({ type, redirectUrl }) {
|
|
5
|
+
const stackApp = useStackApp();
|
|
6
|
+
const project = stackApp.useProject();
|
|
7
|
+
if (!project) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return (_jsx("div", { className: "wl_space-y-4 wl_flex wl_flex-col wl_items-stretch", children: project.oauthProviders.map(({ id }) => (_jsx(OauthButton, { provider: id, type: type, redirectUrl: redirectUrl }, id))) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useRef, useState } from 'react';
|
|
3
|
+
import { HiEye, HiEyeOff } from 'react-icons/hi';
|
|
4
|
+
export const PasswordField = forwardRef(({ id, name, ...props }, ref) => {
|
|
5
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
6
|
+
const inputRef = useRef(null);
|
|
7
|
+
const mergeRef = (node) => {
|
|
8
|
+
if (ref) {
|
|
9
|
+
if (typeof ref === 'function') {
|
|
10
|
+
ref(node);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
ref.current = node;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
inputRef.current = node;
|
|
17
|
+
};
|
|
18
|
+
const onClickReveal = () => {
|
|
19
|
+
setIsOpen(!isOpen);
|
|
20
|
+
const currentInput = inputRef.current;
|
|
21
|
+
if (currentInput) {
|
|
22
|
+
currentInput.focus({ preventScroll: true });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return (_jsxs("div", { className: "wl_relative", children: [_jsx("input", { id: id, ref: mergeRef, name: name, type: isOpen ? 'text' : 'password', autoComplete: "current-password", required: true, className: "wl_input wl_input-bordered wl_w-full", ...props }), _jsx("button", { tabIndex: -1, type: "button", className: "wl_absolute wl_inset-y-0 wl_right-0 wl_flex wl_items-center wl_pr-3", onClick: onClickReveal, "aria-label": isOpen ? 'Mask password' : 'Reveal password', children: isOpen ? _jsx(HiEyeOff, {}) : _jsx(HiEye, {}) })] }));
|
|
26
|
+
});
|
|
27
|
+
PasswordField.displayName = 'PasswordField';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { getPasswordError } from "@stackframe/stack-shared/src/helpers/password";
|
|
5
|
+
import { useStackApp } from "..";
|
|
6
|
+
import { PasswordField } from "./PasswordField";
|
|
7
|
+
import { FormWarningText } from "./FormWarning";
|
|
8
|
+
import RedirectMessageCard from "./RedirectMessageCard";
|
|
9
|
+
import MessageCard from "./MessageCard";
|
|
10
|
+
import CardFrame from "./CardFrame";
|
|
11
|
+
import CardHeader from "./CardHeader";
|
|
12
|
+
export default function PasswordResetInner({ code, fullPage = false }) {
|
|
13
|
+
const [password, setPassword] = useState('');
|
|
14
|
+
const [passwordError, setPasswordError] = useState('');
|
|
15
|
+
const [passwordRepeat, setPasswordRepeat] = useState('');
|
|
16
|
+
const [passwordRepeatError, setPasswordRepeatError] = useState('');
|
|
17
|
+
const [finished, setFinished] = useState(false);
|
|
18
|
+
const [resetError, setResetError] = useState(false);
|
|
19
|
+
const stackApp = useStackApp();
|
|
20
|
+
const onSubmit = async () => {
|
|
21
|
+
if (!password) {
|
|
22
|
+
setPasswordError('Please enter your password');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!passwordRepeat) {
|
|
26
|
+
setPasswordRepeatError('Please repeat your password');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (password !== passwordRepeat) {
|
|
30
|
+
setPasswordRepeatError('Passwords do not match');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const errorMessage = getPasswordError(password);
|
|
34
|
+
if (errorMessage) {
|
|
35
|
+
setPasswordError(errorMessage);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const errorCode = await stackApp.resetPassword({ password, code });
|
|
39
|
+
// this should not happen, the outer component should verify the code before rendering this component
|
|
40
|
+
if (errorCode) {
|
|
41
|
+
setResetError(true);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setFinished(true);
|
|
45
|
+
};
|
|
46
|
+
if (finished) {
|
|
47
|
+
return _jsx(RedirectMessageCard, { type: 'passwordReset', fullPage: fullPage });
|
|
48
|
+
}
|
|
49
|
+
if (resetError) {
|
|
50
|
+
return (_jsx(MessageCard, { title: "Failed to reset password", fullPage: fullPage, children: _jsx("p", { children: "Failed to reset password. Please request a new password reset link" }) }));
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(CardFrame, { fullPage: fullPage, children: [_jsx(CardHeader, { title: "Reset Your Password" }), _jsxs("div", { className: "wl_flex wl_flex-col wl_space-y-2 wl_items-stretch", children: [_jsxs("div", { className: "wl_form-control", children: [_jsx("label", { className: "wl_label", htmlFor: "password", children: "New Password" }), _jsx(PasswordField, { id: "password", name: "password", value: password, onChange: (e) => {
|
|
53
|
+
setPassword(e.target.value);
|
|
54
|
+
setPasswordError('');
|
|
55
|
+
setPasswordRepeatError('');
|
|
56
|
+
} }), _jsx(FormWarningText, { text: passwordError })] }), _jsxs("div", { className: "wl_form-control", children: [_jsx("label", { className: "wl_label", htmlFor: "repeat-password", children: "Repeat New Password" }), _jsx(PasswordField, { id: "repeat-password", name: "repeat-password", value: passwordRepeat, onChange: (e) => {
|
|
57
|
+
setPasswordRepeat(e.target.value);
|
|
58
|
+
setPasswordError('');
|
|
59
|
+
setPasswordRepeatError('');
|
|
60
|
+
} }), _jsx(FormWarningText, { text: passwordRepeatError })] }), _jsx("div", { className: "wl_flex wl_flex-col wl_items-stretch", children: _jsx("button", { className: "wl_btn wl_btn-primary wl_mt-6", onClick: () => void onSubmit(), children: "Reset Password" }) })] })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRouter } from "next/navigation";
|
|
3
|
+
import { useStackApp } from "..";
|
|
4
|
+
import MessageCard from "./MessageCard";
|
|
5
|
+
export default function RedirectMessageCard({ type, fullPage = false, }) {
|
|
6
|
+
const stackApp = useStackApp();
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
let title;
|
|
9
|
+
let url;
|
|
10
|
+
let message = null;
|
|
11
|
+
let buttonText;
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'signedIn': {
|
|
14
|
+
title = "You are already signed in";
|
|
15
|
+
message = 'You are already signed in. You can click the buttont below to sign out if you want to sign in with a different account.';
|
|
16
|
+
url = stackApp.urls.signOut;
|
|
17
|
+
buttonText = "Sign Out";
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
case 'signedOut': {
|
|
21
|
+
title = "You are already signed out";
|
|
22
|
+
url = stackApp.urls.home;
|
|
23
|
+
buttonText = "Go to Home";
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
case 'emailSent': {
|
|
27
|
+
title = "Email Sent";
|
|
28
|
+
message = 'Please check your inbox. If you do not receive the email, please check your spam folder.';
|
|
29
|
+
url = stackApp.urls.home;
|
|
30
|
+
buttonText = "Go to Home";
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case 'passwordReset': {
|
|
34
|
+
title = "Password Reset";
|
|
35
|
+
message = 'Your password has been reset. You can now sign in with your new password.';
|
|
36
|
+
url = stackApp.urls.signIn;
|
|
37
|
+
buttonText = "Go to Sign In";
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case 'emailVerified': {
|
|
41
|
+
title = "Email Verified";
|
|
42
|
+
message = 'Your have successfully verified your email';
|
|
43
|
+
url = stackApp.urls.signIn;
|
|
44
|
+
buttonText = "Go to Home";
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs(MessageCard, { title: title, fullPage: fullPage, children: [message && _jsx("p", { className: 'wl_mb-8', children: message }), _jsx("button", { className: 'wl_btn wl_btn-primary', onClick: () => router.push(url.toString()), children: buttonText })] }));
|
|
49
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as StackProvider } from "./providers/StackProvider";
|
|
2
|
+
export { useUser, useStackApp } from "./lib/hooks";
|
|
3
|
+
export { StackClientApp, StackServerApp, StackAdminApp } from "./lib/stack-app";
|
|
4
|
+
export { default as SignIn } from "./components/SignIn";
|
|
5
|
+
export { default as SignUp } from "./components/SignUp";
|
|
6
|
+
export { default as StackHandler } from "./components/StackHandler";
|
|
7
|
+
export { default as EmailVerification } from "./components/EmailVerification";
|
|
8
|
+
export { default as PasswordReset } from "./components/PasswordReset";
|
|
9
|
+
export { default as ForgotPassword } from "./components/ForgotPassword";
|
|
10
|
+
export { validateEmail } from "./utils/email";
|
|
11
|
+
import '../dist/tailwind.css';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default as StackProvider } from "./providers/StackProvider";
|
|
2
|
+
export { useUser, useStackApp } from "./lib/hooks";
|
|
3
|
+
export { StackClientApp, StackServerApp, StackAdminApp } from "./lib/stack-app";
|
|
4
|
+
export { default as SignIn } from "./components/SignIn";
|
|
5
|
+
export { default as SignUp } from "./components/SignUp";
|
|
6
|
+
export { default as StackHandler } from "./components/StackHandler";
|
|
7
|
+
export { default as EmailVerification } from "./components/EmailVerification";
|
|
8
|
+
export { default as PasswordReset } from "./components/PasswordReset";
|
|
9
|
+
export { default as ForgotPassword } from "./components/ForgotPassword";
|
|
10
|
+
export { validateEmail } from "./utils/email";
|
|
11
|
+
// TODO we can't import it like this
|
|
12
|
+
import '../dist/tailwind.css';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StackClientInterface } from "@stackframe/stack-shared";
|
|
2
|
+
import { TokenStore } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
|
3
|
+
import { SignInErrorCode, SignUpErrorCode } from "@stackframe/stack-shared/dist/utils/types";
|
|
4
|
+
export declare function signInWithOauth(iface: StackClientInterface, { provider, redirectUrl, }: {
|
|
5
|
+
provider: string;
|
|
6
|
+
redirectUrl?: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function callOauthCallback(iface: StackClientInterface, tokenStore: TokenStore, redirectUrl?: string): Promise<void>;
|
|
9
|
+
export declare function signInWithCredential(iface: StackClientInterface, tokenStore: TokenStore, { email, password, redirectUrl, }: {
|
|
10
|
+
email: string;
|
|
11
|
+
password: string;
|
|
12
|
+
redirectUrl?: string;
|
|
13
|
+
}): Promise<SignInErrorCode | undefined>;
|
|
14
|
+
export declare function signUpWithCredential(iface: StackClientInterface, tokenStore: TokenStore, { email, password, redirectUrl, emailVerificationRedirectUrl }: {
|
|
15
|
+
email: string;
|
|
16
|
+
password: string;
|
|
17
|
+
redirectUrl?: string;
|
|
18
|
+
emailVerificationRedirectUrl?: string;
|
|
19
|
+
}): Promise<SignUpErrorCode | undefined>;
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { saveVerifierAndState, getVerifierAndState } from "./cookie";
|
|
2
|
+
import { constructRedirectUrl } from "../utils/url";
|
|
3
|
+
export async function signInWithOauth(iface, { provider, redirectUrl, }) {
|
|
4
|
+
redirectUrl = constructRedirectUrl(redirectUrl);
|
|
5
|
+
const { codeChallenge, state } = await saveVerifierAndState();
|
|
6
|
+
const location = await iface.getOauthUrl(provider, redirectUrl, codeChallenge, state);
|
|
7
|
+
window.location.assign(location);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks if the current URL has the query parameters for an OAuth callback, and if so, removes them.
|
|
11
|
+
*
|
|
12
|
+
* Must be synchronous for the logic in callOauthCallback to work without race conditions.
|
|
13
|
+
*/
|
|
14
|
+
function consumeOauthCallbackQueryParams(expectedState) {
|
|
15
|
+
const requiredParams = ["code", "state"];
|
|
16
|
+
const originalUrl = new URL(window.location.href);
|
|
17
|
+
for (const param of requiredParams) {
|
|
18
|
+
if (!originalUrl.searchParams.has(param)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (expectedState !== originalUrl.searchParams.get("state")) {
|
|
23
|
+
// If the state doesn't match, then the callback wasn't meant for us.
|
|
24
|
+
// Maybe the website uses another OAuth library?
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const newUrl = new URL(originalUrl);
|
|
28
|
+
for (const param of requiredParams) {
|
|
29
|
+
newUrl.searchParams.delete(param);
|
|
30
|
+
}
|
|
31
|
+
// let's get rid of the authorization code in the history as we
|
|
32
|
+
// don't redirect to `redirectUrl` if there's a validation error
|
|
33
|
+
// (as the redirectUrl might be malicious!).
|
|
34
|
+
//
|
|
35
|
+
// We use history.replaceState instead of location.assign(...) to
|
|
36
|
+
// prevent an unnecessary reload
|
|
37
|
+
window.history.replaceState({}, "", newUrl.toString());
|
|
38
|
+
return { newUrl, originalUrl };
|
|
39
|
+
}
|
|
40
|
+
export async function callOauthCallback(iface, tokenStore, redirectUrl) {
|
|
41
|
+
// note: this part of the function (until the return) needs
|
|
42
|
+
// to be synchronous, to prevent race conditions when
|
|
43
|
+
// callOauthCallback is called multiple times in parallel
|
|
44
|
+
const { codeVerifier, state } = getVerifierAndState();
|
|
45
|
+
const consumeResult = consumeOauthCallbackQueryParams(state);
|
|
46
|
+
if (!consumeResult) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!codeVerifier || !state) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// the rest can be asynchronous (we now know that we are the
|
|
53
|
+
// intended recipient of the callback)
|
|
54
|
+
const { newUrl, originalUrl } = consumeResult;
|
|
55
|
+
if (!redirectUrl) {
|
|
56
|
+
redirectUrl = newUrl.toString();
|
|
57
|
+
}
|
|
58
|
+
redirectUrl = redirectUrl.split("#")[0]; // remove hash
|
|
59
|
+
try {
|
|
60
|
+
await iface.callOauthCallback(originalUrl.searchParams, redirectUrl, codeVerifier, state, tokenStore);
|
|
61
|
+
// reload/redirect so the server can update now that the user is signed in
|
|
62
|
+
window.location.assign(redirectUrl);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
console.error("Error signing in during OAuth callback", e);
|
|
66
|
+
throw new Error("Error signing in. Please try again.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function signInWithCredential(iface, tokenStore, { email, password, redirectUrl, }) {
|
|
70
|
+
const errorCode = await iface.signInWithCredential(email, password, tokenStore);
|
|
71
|
+
if (!errorCode) {
|
|
72
|
+
redirectUrl = constructRedirectUrl(redirectUrl);
|
|
73
|
+
window.location.assign(redirectUrl);
|
|
74
|
+
}
|
|
75
|
+
return errorCode;
|
|
76
|
+
}
|
|
77
|
+
export async function signUpWithCredential(iface, tokenStore, { email, password, redirectUrl, emailVerificationRedirectUrl = "/" }) {
|
|
78
|
+
emailVerificationRedirectUrl = constructRedirectUrl(emailVerificationRedirectUrl);
|
|
79
|
+
const errorCode = await iface.signUpWithCredential(email, password, emailVerificationRedirectUrl, tokenStore);
|
|
80
|
+
if (!errorCode) {
|
|
81
|
+
redirectUrl = constructRedirectUrl(redirectUrl);
|
|
82
|
+
window.location.assign(redirectUrl);
|
|
83
|
+
}
|
|
84
|
+
return errorCode;
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function getCookie(name: string): string | null;
|
|
2
|
+
export declare function setOrDeleteCookie(name: string, value: string | null): void;
|
|
3
|
+
export declare function deleteCookie(name: string): void;
|
|
4
|
+
export declare function setCookie(name: string, value: string): void;
|
|
5
|
+
export declare function saveVerifierAndState(): Promise<{
|
|
6
|
+
codeChallenge: string;
|
|
7
|
+
state: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function getVerifierAndState(): {
|
|
10
|
+
codeVerifier: string | null;
|
|
11
|
+
state: string | null;
|
|
12
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { generateRandomCodeVerifier, generateRandomState, calculatePKCECodeChallenge } from "oauth4webapi";
|
|
2
|
+
import Cookies from "js-cookie";
|
|
3
|
+
import { isClient } from "../utils/next";
|
|
4
|
+
export function getCookie(name) {
|
|
5
|
+
// TODO the differentiating factor should be RCC vs. RSC, not whether it's a client
|
|
6
|
+
if (isClient()) {
|
|
7
|
+
return Cookies.get(name) ?? null;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
const { cookies } = require("next/headers");
|
|
11
|
+
return cookies().get(name)?.value ?? null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function setOrDeleteCookie(name, value) {
|
|
15
|
+
if (value === null) {
|
|
16
|
+
deleteCookie(name);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
setCookie(name, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function deleteCookie(name) {
|
|
23
|
+
// TODO the differentiating factor should be RCC vs. RSC, not whether it's a client
|
|
24
|
+
if (isClient()) {
|
|
25
|
+
Cookies.remove(name);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const { cookies } = require("next/headers");
|
|
29
|
+
cookies().delete(name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function setCookie(name, value) {
|
|
33
|
+
// TODO the differentiating factor should be RCC vs. RSC, not whether it's a client
|
|
34
|
+
if (isClient()) {
|
|
35
|
+
Cookies.set(name, value);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const { cookies } = require("next/headers");
|
|
39
|
+
cookies().set(name, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function saveVerifierAndState() {
|
|
43
|
+
const codeVerifier = generateRandomCodeVerifier();
|
|
44
|
+
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
45
|
+
const state = generateRandomState();
|
|
46
|
+
setCookie("stack-code-verifier", codeVerifier);
|
|
47
|
+
setCookie("stack-state", state);
|
|
48
|
+
return {
|
|
49
|
+
codeChallenge,
|
|
50
|
+
state,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function getVerifierAndState() {
|
|
54
|
+
const codeVerifier = getCookie("stack-code-verifier");
|
|
55
|
+
const state = getCookie("stack-state");
|
|
56
|
+
return {
|
|
57
|
+
codeVerifier,
|
|
58
|
+
state,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CurrentUser, GetUserOptions, StackClientApp } from "./stack-app";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the current user object. Equivalent to `useStackApp().useUser()`.
|
|
4
|
+
*
|
|
5
|
+
* @returns the current user
|
|
6
|
+
*/
|
|
7
|
+
export declare function useUser(options: GetUserOptions & {
|
|
8
|
+
or: 'redirect';
|
|
9
|
+
}): CurrentUser;
|
|
10
|
+
export declare function useUser(options: GetUserOptions & {
|
|
11
|
+
or: 'throw';
|
|
12
|
+
}): CurrentUser;
|
|
13
|
+
export declare function useUser(options?: GetUserOptions): CurrentUser | null;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the current Stack app associated with the StackProvider.
|
|
16
|
+
*
|
|
17
|
+
* @returns the current Stack app
|
|
18
|
+
*/
|
|
19
|
+
export declare function useStackApp<ProjectId extends string>(options?: {
|
|
20
|
+
projectIdMustMatch?: ProjectId;
|
|
21
|
+
}): StackClientApp<true, ProjectId>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StackContext } from "../providers/StackProviderClient";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
export function useUser(options) {
|
|
4
|
+
return useStackApp().useUser(options);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns the current Stack app associated with the StackProvider.
|
|
8
|
+
*
|
|
9
|
+
* @returns the current Stack app
|
|
10
|
+
*/
|
|
11
|
+
export function useStackApp(options = {}) {
|
|
12
|
+
const context = useContext(StackContext);
|
|
13
|
+
if (context === null) {
|
|
14
|
+
throw new Error("useStackApp must be used within a StackProvider");
|
|
15
|
+
}
|
|
16
|
+
const stackApp = context.app;
|
|
17
|
+
if (options.projectIdMustMatch && stackApp.projectId !== options.projectIdMustMatch) {
|
|
18
|
+
throw new Error("Unexpected project ID in useStackApp: " + stackApp.projectId);
|
|
19
|
+
}
|
|
20
|
+
return stackApp;
|
|
21
|
+
}
|