mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
Add Supertoken login page
This commit is contained in:
@@ -29,7 +29,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "=6.20.1",
|
||||
"sonner": "^1.0.3"
|
||||
"sonner": "^1.0.3",
|
||||
"supertokens-web-js": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
|
||||
@@ -1,33 +1,80 @@
|
||||
import { useZodForm } from '@sd/client';
|
||||
import { Button, Form, Input, z } from '@sd/ui';
|
||||
|
||||
import { Eye, EyeClosed } from '@phosphor-icons/react';
|
||||
import { useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { signIn, signUp } from 'supertokens-web-js/recipe/emailpassword';
|
||||
import { useZodForm } from '@sd/client';
|
||||
import { Button, Form, Input, toast, z } from '@sd/ui';
|
||||
|
||||
async function signInClicked(email: string, password: string) {
|
||||
try {
|
||||
const response = await signIn({
|
||||
formFields: [
|
||||
{
|
||||
id: 'email',
|
||||
value: email
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
value: password
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log('[signInClicked] response', response);
|
||||
|
||||
if (response.status === 'FIELD_ERROR') {
|
||||
response.formFields.forEach((formField) => {
|
||||
if (formField.id === 'email') {
|
||||
// Email validation failed (for example incorrect email syntax).
|
||||
toast.error(formField.error);
|
||||
}
|
||||
});
|
||||
} else if (response.status === 'WRONG_CREDENTIALS_ERROR') {
|
||||
toast.error('Email & password combination is incorrect.');
|
||||
} else if (response.status === 'SIGN_IN_NOT_ALLOWED') {
|
||||
// the reason string is a user friendly message
|
||||
// about what went wrong. It can also contain a support code which users
|
||||
// can tell you so you know why their sign in was not allowed.
|
||||
toast.error(response.reason);
|
||||
} else {
|
||||
// sign in successful. The session tokens are automatically handled by
|
||||
// the frontend SDK.
|
||||
console.log('Sign in successful');
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.isSuperTokensGeneralError === true) {
|
||||
// this may be a custom error message sent from the API by you.
|
||||
toast.error(err.message);
|
||||
} else {
|
||||
toast.error('Oops! Something went wrong.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
})
|
||||
password: z.string().min(6)
|
||||
});
|
||||
|
||||
const Login = () => {
|
||||
const form = useZodForm(
|
||||
{
|
||||
schema: LoginSchema,
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Form
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const form = useZodForm({
|
||||
schema: LoginSchema,
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Form
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
// handle login submission
|
||||
console.log(data);
|
||||
})}
|
||||
await signInClicked(data.email, data.password);
|
||||
})}
|
||||
form={form}
|
||||
>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<Controller
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
@@ -47,33 +94,47 @@ const Login = () => {
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Password"
|
||||
error={Boolean(form.formState.errors.password?.message)}
|
||||
type="password"
|
||||
className='w-full'
|
||||
disabled={form.formState.isSubmitting}
|
||||
/>
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Password"
|
||||
error={Boolean(form.formState.errors.password?.message)}
|
||||
className="w-full"
|
||||
disabled={form.formState.isSubmitting}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
onPaste={(e) => {
|
||||
const pastedText = e.clipboardData.getData('text');
|
||||
field.onChange(pastedText);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="gray"
|
||||
className="absolute right-2"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{!showPassword ? <EyeClosed /> : <Eye />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.password && (
|
||||
<p className="text-xs text-red-500">{form.formState.errors.password.message}</p>
|
||||
)}
|
||||
<Button
|
||||
<Button
|
||||
type="submit"
|
||||
className='mx-auto mt-2 w-full'
|
||||
className="mx-auto mt-2 w-full"
|
||||
variant="accent"
|
||||
onClick={form.handleSubmit((data) => {
|
||||
onClick={form.handleSubmit(async (data) => {
|
||||
console.log(data);
|
||||
await signInClicked(data.email, data.password);
|
||||
})}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
|
||||
@@ -1,105 +1,181 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button, Form, Input, z } from '@sd/ui';
|
||||
|
||||
import { Eye, EyeClosed } from '@phosphor-icons/react';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { signUp } from 'supertokens-web-js/recipe/emailpassword';
|
||||
import { Button, Form, Input, toast, z } from '@sd/ui';
|
||||
|
||||
const RegisterSchema = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
confirmPassword: z.string().min(6)
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword']
|
||||
});
|
||||
type RegisterType = z.infer<typeof RegisterSchema>;
|
||||
|
||||
const RegisterSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
confirmPassword: z.string().min(6),
|
||||
}).refine(data => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword']
|
||||
})
|
||||
type RegisterType = z.infer<typeof RegisterSchema>
|
||||
async function signUpClicked(email: string, password: string) {
|
||||
try {
|
||||
const response = await signUp({
|
||||
formFields: [
|
||||
{
|
||||
id: 'email',
|
||||
value: email
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
value: password
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (response.status === 'FIELD_ERROR') {
|
||||
// one of the input formFields failed validaiton
|
||||
response.formFields.forEach((formField) => {
|
||||
if (formField.id === 'email') {
|
||||
// Email validation failed (for example incorrect email syntax),
|
||||
// or the email is not unique.
|
||||
toast.error(formField.error);
|
||||
} else if (formField.id === 'password') {
|
||||
// Password validation failed.
|
||||
// Maybe it didn't match the password strength
|
||||
toast.error(formField.error);
|
||||
}
|
||||
});
|
||||
} else if (response.status === 'SIGN_UP_NOT_ALLOWED') {
|
||||
// the reason string is a user friendly message
|
||||
// about what went wrong. It can also contain a support code which users
|
||||
// can tell you so you know why their sign up was not allowed.
|
||||
toast.error(response.reason);
|
||||
} else {
|
||||
// sign up successful. The session tokens are automatically handled by
|
||||
// the frontend SDK.
|
||||
window.location.href = '/homepage';
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.isSuperTokensGeneralError === true) {
|
||||
// this may be a custom error message sent from the API by you.
|
||||
toast.error(err.message);
|
||||
} else {
|
||||
toast.error('Oops! Something went wrong.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Register = () => {
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// useZodForm seems to be out-dated or needs
|
||||
//fixing as it does not support the schema using zod.refine
|
||||
const form = useForm<RegisterType>(
|
||||
{
|
||||
const form = useForm<RegisterType>({
|
||||
resolver: zodResolver(RegisterSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
})
|
||||
});
|
||||
return (
|
||||
<Form
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
// handle login submission
|
||||
return console.log(data);
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
// handle login submission
|
||||
console.log(data);
|
||||
await signUpClicked(data.email, data.password);
|
||||
})}
|
||||
form={form}
|
||||
form={form}
|
||||
>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Email"
|
||||
error={Boolean(form.formState.errors.email?.message)}
|
||||
type="email"
|
||||
disabled={form.formState.isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.email && (
|
||||
<p className="text-xs text-red-500">{form.formState.errors.email.message}</p>
|
||||
)}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Password"
|
||||
error={Boolean(form.formState.errors.password?.message)}
|
||||
type="password"
|
||||
className='w-full'
|
||||
disabled={form.formState.isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.password && (
|
||||
<p className="text-xs text-red-500">{form.formState.errors.password.message}</p>
|
||||
)}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Confirm Password"
|
||||
error={Boolean(form.formState.errors.confirmPassword?.message)}
|
||||
type="password"
|
||||
className='w-full'
|
||||
disabled={form.formState.isSubmitting}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Email"
|
||||
error={Boolean(form.formState.errors.email?.message)}
|
||||
type="email"
|
||||
disabled={form.formState.isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.confirmPassword && (
|
||||
<p className="text-xs text-red-500">{form.formState.errors.confirmPassword.message}</p>
|
||||
)}
|
||||
{form.formState.errors.email && (
|
||||
<p className="text-xs text-red-500">{form.formState.errors.email.message}</p>
|
||||
)}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Password"
|
||||
error={Boolean(form.formState.errors.password?.message)}
|
||||
className="w-full"
|
||||
disabled={form.formState.isSubmitting}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
onPaste={(e) => {
|
||||
const pastedText = e.clipboardData.getData('text');
|
||||
field.onChange(pastedText);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="gray"
|
||||
className="absolute right-2"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{!showPassword ? <EyeClosed /> : <Eye />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.password && (
|
||||
<p className="text-xs text-red-500">{form.formState.errors.password.message}</p>
|
||||
)}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Confirm Password"
|
||||
error={Boolean(form.formState.errors.confirmPassword?.message)}
|
||||
className="w-full"
|
||||
disabled={form.formState.isSubmitting}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
<Button
|
||||
variant="gray"
|
||||
className="absolute right-2"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{!showPassword ? <EyeClosed /> : <Eye />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.confirmPassword && (
|
||||
<p className="text-xs text-red-500">
|
||||
{form.formState.errors.confirmPassword.message}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
className='mx-auto mt-2 w-full'
|
||||
variant="accent"
|
||||
onClick={form.handleSubmit((data) => {
|
||||
console.log(data);
|
||||
})}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
type="submit"
|
||||
className="mx-auto mt-2 w-full"
|
||||
variant="accent"
|
||||
onClick={form.handleSubmit(async (data) => {
|
||||
console.log(data);
|
||||
await signUpClicked(data.email, data.password);
|
||||
})}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { CookieHandlerInterface } from "supertokens-website/utils/cookieHandler/types";
|
||||
|
||||
const frontendCookiesKey = "frontendCookies";
|
||||
|
||||
function getCookiesFromStorage(): string {
|
||||
const cookiesFromStorage = window.localStorage.getItem(frontendCookiesKey);
|
||||
|
||||
if (cookiesFromStorage === null) {
|
||||
window.localStorage.setItem(frontendCookiesKey, "[]");
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Because we store cookies in local storage, we need to manually check
|
||||
* for expiry before returning all cookies
|
||||
*/
|
||||
const cookieArrayInStorage: string[] = JSON.parse(cookiesFromStorage);
|
||||
const cookieArrayToReturn: string[] = [];
|
||||
|
||||
for (let cookieIndex = 0; cookieIndex < cookieArrayInStorage.length; cookieIndex++) {
|
||||
const currentCookieString = cookieArrayInStorage[cookieIndex];
|
||||
const parts = currentCookieString?.split(";") ?? [];
|
||||
let expirationString: string = "";
|
||||
|
||||
for (let partIndex = 0; partIndex < parts.length; partIndex++) {
|
||||
const currentPart = parts[partIndex];
|
||||
|
||||
if (currentPart?.toLocaleLowerCase().includes("expires=")) {
|
||||
expirationString = currentPart;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (expirationString !== "") {
|
||||
const expirationValueString = expirationString.split("=")[1];
|
||||
const expirationDate = expirationValueString ? new Date(expirationValueString) : null;
|
||||
const currentTimeInMillis = Date.now();
|
||||
|
||||
// if the cookie has expired, we skip it
|
||||
if (expirationDate && expirationDate.getTime() < currentTimeInMillis) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentCookieString !== undefined) {
|
||||
cookieArrayToReturn.push(currentCookieString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After processing and removing expired cookies we need to update the cookies
|
||||
* in storage so we dont have to process the expired ones again
|
||||
*/
|
||||
window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookieArrayToReturn));
|
||||
|
||||
return cookieArrayToReturn.join("; ");
|
||||
}
|
||||
|
||||
function setCookieToStorage(cookieString: string) {
|
||||
const cookieName = cookieString.split(";")[0]?.split("=")[0];
|
||||
const cookiesFromStorage = window.localStorage.getItem(frontendCookiesKey);
|
||||
let cookiesArray: string[] = [];
|
||||
|
||||
if (cookiesFromStorage !== null) {
|
||||
const cookiesArrayFromStorage: string[] = JSON.parse(cookiesFromStorage);
|
||||
cookiesArray = cookiesArrayFromStorage;
|
||||
}
|
||||
|
||||
let cookieIndex = -1;
|
||||
|
||||
for (let i = 0; i < cookiesArray.length; i++) {
|
||||
const currentCookie = cookiesArray[i];
|
||||
|
||||
if (currentCookie?.indexOf(`${cookieName}=`) !== -1) {
|
||||
cookieIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie with the same name already exists (index != -1) then we
|
||||
* need to remove the old value and replace it with the new one.
|
||||
*
|
||||
* If it does not exist then simply add the new cookie
|
||||
*/
|
||||
if (cookieIndex !== -1) {
|
||||
cookiesArray[cookieIndex] = cookieString;
|
||||
} else {
|
||||
cookiesArray.push(cookieString);
|
||||
}
|
||||
|
||||
window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookiesArray));
|
||||
}
|
||||
|
||||
export default function getCookieHandler(original: CookieHandlerInterface): CookieHandlerInterface {
|
||||
return {
|
||||
...original,
|
||||
getCookie: async function () {
|
||||
const cookies = getCookiesFromStorage();
|
||||
return cookies;
|
||||
},
|
||||
setCookie: async function (cookieString: string) {
|
||||
setCookieToStorage(cookieString);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { WindowHandlerInterface } from "supertokens-website/utils/windowHandler/types";
|
||||
|
||||
/**
|
||||
* This example app uses HashRouter from react-router-dom. The SuperTokens SDK relies on
|
||||
* some window properties like location hash, query params etc. Because HashRouter places
|
||||
* everything other than the website base in the location hash, we need to add custom
|
||||
* handling for some of the properties of the Window API
|
||||
*/
|
||||
export default function getWindowHandler(original: WindowHandlerInterface): WindowHandlerInterface {
|
||||
return {
|
||||
...original,
|
||||
location: {
|
||||
...original.location,
|
||||
getSearch: function () {
|
||||
const currentURL = window.location.href;
|
||||
const firstQuestionMarkIndex = currentURL.indexOf("?");
|
||||
|
||||
if (firstQuestionMarkIndex !== -1) {
|
||||
// Return the query string from the url
|
||||
let queryString = currentURL.substring(firstQuestionMarkIndex);
|
||||
|
||||
// Remove any hash
|
||||
if (queryString.includes("#")) {
|
||||
queryString = queryString.split("#")[0] ?? "";
|
||||
}
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
getHash: function () {
|
||||
// Location hash always starts with a #, when returning we prepend it
|
||||
let locationHash = window.location.hash;
|
||||
|
||||
if (locationHash === "") {
|
||||
return "#";
|
||||
}
|
||||
|
||||
if (locationHash.startsWith("#")) {
|
||||
// Remove the starting pound symbol
|
||||
locationHash = locationHash.substring(1);
|
||||
}
|
||||
|
||||
if (!locationHash.includes("#")) {
|
||||
// The remaining string did not have any "#" character
|
||||
return "#";
|
||||
}
|
||||
|
||||
const locationSplit = locationHash.split("#");
|
||||
|
||||
if (locationSplit.length < 2) {
|
||||
// The string contains a "#" but is followed by nothing
|
||||
return "#";
|
||||
}
|
||||
|
||||
return "#" + locationSplit[1];
|
||||
},
|
||||
getOrigin: function () {
|
||||
return "http://localhost:8001";
|
||||
},
|
||||
getHostName: function () {
|
||||
return "localhost";
|
||||
},
|
||||
getPathName: function () {
|
||||
let locationHash = window.location.hash;
|
||||
|
||||
if (locationHash === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (locationHash.startsWith("#")) {
|
||||
// Remove the starting pound symbol
|
||||
locationHash = locationHash.substring(1);
|
||||
}
|
||||
|
||||
locationHash = locationHash.split("?")[0] ?? "";
|
||||
|
||||
if (locationHash.includes("#")) {
|
||||
// Remove location hash
|
||||
locationHash = locationHash.split("#")[0] ?? "";
|
||||
}
|
||||
|
||||
return locationHash;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { PropsWithChildren, Suspense } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { RouterProvider, RouterProviderProps } from 'react-router-dom';
|
||||
import SuperTokens from 'supertokens-web-js';
|
||||
import EmailPassword from 'supertokens-web-js/recipe/emailpassword';
|
||||
import Session from 'supertokens-web-js/recipe/session';
|
||||
import ThirdParty from 'supertokens-web-js/recipe/thirdparty';
|
||||
import {
|
||||
InteropProviderReact,
|
||||
P2PContextProvider,
|
||||
@@ -15,6 +19,8 @@ import {
|
||||
import { toast, TooltipProvider } from '@sd/ui';
|
||||
|
||||
import { createRoutes } from './app';
|
||||
import getCookieHandler from './app/$libraryId/settings/client/account/handlers/cookieHandler';
|
||||
import getWindowHandler from './app/$libraryId/settings/client/account/handlers/windowHandler';
|
||||
import { SpacedropProvider } from './app/$libraryId/Spacedrop';
|
||||
import i18n from './app/I18n';
|
||||
import { Devtools } from './components/Devtools';
|
||||
@@ -42,6 +48,22 @@ import('@sentry/browser').then(({ init, Integrations }) => {
|
||||
});
|
||||
});
|
||||
|
||||
SuperTokens.init({
|
||||
// enableDebugLogs: true,
|
||||
appInfo: {
|
||||
apiDomain: 'http://localhost:9000',
|
||||
apiBasePath: '/api/auth',
|
||||
appName: 'Spacedrive Auth Service'
|
||||
},
|
||||
cookieHandler: getCookieHandler,
|
||||
windowHandler: getWindowHandler,
|
||||
recipeList: [
|
||||
Session.init({ tokenTransferMethod: 'header' }),
|
||||
EmailPassword.init(),
|
||||
ThirdParty.init()
|
||||
]
|
||||
});
|
||||
|
||||
export type Router = RouterProviderProps['router'];
|
||||
|
||||
export function SpacedriveRouterProvider(props: {
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"rooks": "^7.14.1",
|
||||
"solid-js": "^1.8.8",
|
||||
"solid-refresh": "^0.6.3",
|
||||
"supertokens-web-js": "^0.13.0",
|
||||
"use-count-up": "^3.0.1",
|
||||
"use-debounce": "^9.0.4",
|
||||
"use-resize-observer": "^9.1.0",
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user