mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 09:48:44 +00:00
authentication signals and auto login
This commit is contained in:
1 parent
43ab6a88a7
commit
a52013fd0e
19 files changed
+440
-10
No files matched your search
+13
-6
@@ -3,7 +3,10 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html{
|
||||
:root {
|
||||
@apply bg-ui-background text-white;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
h1 {
|
||||
@@ -18,7 +21,10 @@
|
||||
h4 {
|
||||
@apply text-lg font-medium;
|
||||
}
|
||||
/* h5 { } */
|
||||
h5 {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-sm;
|
||||
}
|
||||
@@ -26,12 +32,13 @@
|
||||
@apply text-sm font-light;
|
||||
}
|
||||
a {
|
||||
@apply font-light hover:text-primary-400;
|
||||
@apply font-light text-primary-500 hover:text-primary-300;
|
||||
}
|
||||
a:visited {
|
||||
@apply hover:text-primary-700;
|
||||
@apply text-primary-700 hover:text-primary-500;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply px-2 py-1 border-0 outline-none -outline-offset-2 focus:outline-primary-400 focus:ring-0 ;
|
||||
@apply border-0 px-2 py-1 outline-none -outline-offset-2 focus:outline-primary-500 focus:ring-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./withProtectedRoute";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ComponentType, useEffect, useRef } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useSignalEffect } from "@preact/signals-react/runtime";
|
||||
import { Spinner } from "~/components/ui";
|
||||
import { authentication, persistLogin } from "~/signals";
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
|
||||
export const withProtectionRoute = <P extends object>(
|
||||
Component: ComponentType<P>,
|
||||
) =>
|
||||
function ProtectionRoute(rest: P) {
|
||||
useSignalEffect(() => {
|
||||
persistLogin();
|
||||
});
|
||||
|
||||
const { status, userInfo } = authentication;
|
||||
//render according to auth status
|
||||
if (
|
||||
status.value === AUTH_STATUS.INIT ||
|
||||
status.value === AUTH_STATUS.LOGGING ||
|
||||
status.value === AUTH_STATUS.GET_USER_INFO
|
||||
) {
|
||||
return (
|
||||
<div className="fixed flex h-full w-full flex-col items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
status.value === AUTH_STATUS.LOGGED_IN &&
|
||||
userInfo.value &&
|
||||
userInfo.value.can_access_studio
|
||||
) {
|
||||
return <Component {...rest} />;
|
||||
}
|
||||
|
||||
return <RedirectToLogin />;
|
||||
};
|
||||
|
||||
const RedirectToLogin = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const timeoutTimer = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!timeoutTimer.current) {
|
||||
timeoutTimer.current = setTimeout(
|
||||
() => navigate(`/login?redirect=${pathname}`),
|
||||
2000,
|
||||
);
|
||||
}
|
||||
}, [navigate, pathname]);
|
||||
return (
|
||||
<div className="fixed flex h-full w-full flex-col items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./spinner";
|
||||
@@ -0,0 +1,14 @@
|
||||
import { faSpinnerThird } from "@fortawesome/pro-thin-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
export const Spinner = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<div className={twMerge("aspect-square size-10", className)}>
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="size-full animate-spin"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export enum AUTH_STATUS {
|
||||
INIT = "INIT",
|
||||
NO_ACCESS = "no_access",
|
||||
LOGGED_IN = "logged_in",
|
||||
LOGGING = "logging",
|
||||
GET_USER_INFO = "get_user_info",
|
||||
LOGGED_OUT = "logged_out",
|
||||
}
|
||||
|
||||
export enum AUTH_ERROR_FALLBACKS {
|
||||
CreateSessionError = "Unknown Error during Create Session",
|
||||
DestorySessionError = "Unknown Error during Destroy Session",
|
||||
GetSessionError = "Unknown Error During Get Session",
|
||||
Unauthorized = "User Unauthorized",
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum SubscriptionNamespace {
|
||||
FAKEYOU = "fakeyou",
|
||||
}
|
||||
export enum SubscriptionProduct {
|
||||
PLUS = "fakeyou_plus",
|
||||
PRO = "fakeyou_pro",
|
||||
ELITE = "fakeyou_elite",
|
||||
}
|
||||
export enum LoyaltyProgram {
|
||||
CONTRIBUTOR = "fakeyou_contributor",
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum USER_FEATURE_FLAGS {
|
||||
EXPLORE_MEDIA = "explore_media",
|
||||
STUDIO = "studio",
|
||||
UPLOAD_3D = "upload_3d",
|
||||
VIDEO_STYLE_TRANSFER = "video_style_transfer",
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./Authentication";
|
||||
export * from "./Billing";
|
||||
export * from "./Users";
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
LoyaltyProgram,
|
||||
SubscriptionNamespace,
|
||||
SubscriptionProduct,
|
||||
} from "~/enums";
|
||||
|
||||
export interface ActiveSubscriptions {
|
||||
// user's list of subscriptions
|
||||
active_subscriptions: Subscription[];
|
||||
// special programs that the user could be in
|
||||
maybe_loyalty_program?: LoyaltyProgram;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
namespace: SubscriptionNamespace;
|
||||
product_slug: SubscriptionProduct;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { USER_FEATURE_FLAGS } from "~/enums";
|
||||
|
||||
export interface CoreInfo {
|
||||
default_avatar: {
|
||||
color_index: number;
|
||||
image_index: number;
|
||||
};
|
||||
display_name: string;
|
||||
gravatar_hash: string;
|
||||
user_token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
user_token: string;
|
||||
username: string;
|
||||
display_name: string;
|
||||
email_gravatar_hash: string;
|
||||
|
||||
core_info: CoreInfo;
|
||||
|
||||
// Rollout feature flags
|
||||
can_access_studio: boolean;
|
||||
maybe_feature_flags: USER_FEATURE_FLAGS[];
|
||||
|
||||
// Usage
|
||||
can_use_tts: boolean;
|
||||
can_use_w2l: boolean;
|
||||
can_delete_own_tts_results: boolean;
|
||||
can_delete_own_w2l_results: boolean;
|
||||
can_delete_own_account: boolean;
|
||||
|
||||
// Contribution
|
||||
can_upload_tts_models: boolean;
|
||||
can_upload_w2l_templates: boolean;
|
||||
can_delete_own_tts_models: boolean;
|
||||
can_delete_own_w2l_templates: boolean;
|
||||
|
||||
// Moderation
|
||||
can_approve_w2l_templates: boolean;
|
||||
can_edit_other_users_profiles: boolean;
|
||||
can_edit_other_users_tts_models: boolean;
|
||||
can_edit_other_users_w2l_templates: boolean;
|
||||
can_delete_other_users_tts_models: boolean;
|
||||
can_delete_other_users_tts_results: boolean;
|
||||
can_delete_other_users_w2l_templates: boolean;
|
||||
can_delete_other_users_w2l_results: boolean;
|
||||
can_ban_users: boolean;
|
||||
can_delete_users: boolean;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Billing";
|
||||
export * from "./Users";
|
||||
+10
-3
@@ -1,3 +1,10 @@
|
||||
export const Main = () => {
|
||||
return <div>Main</div>;
|
||||
};
|
||||
import { withProtectionRoute } from "~/components/hoc";
|
||||
import { authentication } from "~/signals";
|
||||
|
||||
export const Main = withProtectionRoute(() => {
|
||||
return (
|
||||
<div>
|
||||
<p>you are logged in as {authentication.userInfo.value?.display_name}</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { computed, signal } from "@preact/signals-core";
|
||||
import { ActiveSubscriptions, UserInfo } from "~/models";
|
||||
import { AUTH_STATUS, LoyaltyProgram, USER_FEATURE_FLAGS } from "~/enums";
|
||||
|
||||
const status = signal<AUTH_STATUS>(AUTH_STATUS.INIT);
|
||||
const userInfo = signal<UserInfo | undefined>(undefined);
|
||||
const activeSubs = signal<ActiveSubscriptions | undefined>(undefined);
|
||||
|
||||
const canUpload3D = computed(() => {
|
||||
if (!userInfo.value || !userInfo.value.maybe_feature_flags) {
|
||||
return undefined;
|
||||
}
|
||||
return userInfo.value.maybe_feature_flags.includes(
|
||||
USER_FEATURE_FLAGS.UPLOAD_3D,
|
||||
);
|
||||
});
|
||||
|
||||
const hasAccess = computed(() => {
|
||||
if (
|
||||
userInfo.value === undefined ||
|
||||
userInfo.value.can_access_studio === undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return userInfo.value.can_access_studio;
|
||||
});
|
||||
|
||||
const hasPremium = computed(() => {
|
||||
if (activeSubs.value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
activeSubs.value.active_subscriptions.length > 0 ||
|
||||
activeSubs.value.maybe_loyalty_program === LoyaltyProgram.CONTRIBUTOR
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
export const authentication = {
|
||||
status,
|
||||
userInfo,
|
||||
activeSubs,
|
||||
canUpload3D,
|
||||
hasAccess,
|
||||
hasPremium,
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { authentication } from "./authentication";
|
||||
import { UsersApi } from "~/Classes/ApiManager/UsersApi";
|
||||
import { BillingApi } from "~/Classes/ApiManager/BillingApi";
|
||||
|
||||
import {
|
||||
updateActiveSubscriptions,
|
||||
updateAuthStatus,
|
||||
updateUserInfo,
|
||||
setLogoutStates,
|
||||
} from "./utilities";
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
|
||||
export const logout = async (
|
||||
failureCallback?: (errorMessage: string) => void,
|
||||
) => {
|
||||
const usersApi = new UsersApi();
|
||||
const logoutResponse = await usersApi.Logout();
|
||||
if (!logoutResponse.success && failureCallback) {
|
||||
failureCallback(
|
||||
logoutResponse.errorMessage || "Unknown Error during Destroy Session",
|
||||
);
|
||||
}
|
||||
// if success, nothing
|
||||
// regarldess of success/fail, clear the state and localstorage
|
||||
setLogoutStates();
|
||||
};
|
||||
|
||||
export const login = async ({
|
||||
usernameOrEmail,
|
||||
password,
|
||||
}: {
|
||||
usernameOrEmail: string;
|
||||
password: string;
|
||||
failureCallback?: () => void;
|
||||
}) => {
|
||||
updateAuthStatus(AUTH_STATUS.LOGGING);
|
||||
|
||||
const usersApi = new UsersApi();
|
||||
const loginResponse = await usersApi.Login({ usernameOrEmail, password });
|
||||
if (!loginResponse.success || !loginResponse.data) {
|
||||
setLogoutStates();
|
||||
return;
|
||||
}
|
||||
|
||||
// technically user is login with the system now, HOWEVER,
|
||||
// in storyteller studio, only having a sesison is not enough,
|
||||
// we need session info and active subscription info as well
|
||||
getUserInfoAndSubcriptions();
|
||||
};
|
||||
|
||||
export const signUp = async ({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
}: {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}) => {
|
||||
updateAuthStatus(AUTH_STATUS.LOGGING);
|
||||
|
||||
const usersApi = new UsersApi();
|
||||
const response = await usersApi.Signup({
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
username,
|
||||
});
|
||||
console.log(response);
|
||||
if (!response.success || !response.data) {
|
||||
setLogoutStates();
|
||||
return response.errorMessage ?? "Unknown error";
|
||||
}
|
||||
|
||||
// technically user is login with the system now, HOWEVER,
|
||||
// in storyteller studio, only having a sesison is not enough,
|
||||
// we need session info and active subscription info as well
|
||||
getUserInfoAndSubcriptions();
|
||||
return "";
|
||||
};
|
||||
|
||||
export const persistLogin = async () => {
|
||||
//Only run First Load, return if not
|
||||
if (authentication.status.value !== AUTH_STATUS.INIT) {
|
||||
return;
|
||||
}
|
||||
getUserInfoAndSubcriptions();
|
||||
};
|
||||
|
||||
async function getUserInfoAndSubcriptions() {
|
||||
updateAuthStatus(AUTH_STATUS.GET_USER_INFO);
|
||||
const usersApi = new UsersApi();
|
||||
const sessionResponse = await usersApi.GetSession();
|
||||
if (
|
||||
!sessionResponse.success ||
|
||||
!sessionResponse.data ||
|
||||
!sessionResponse.data.user
|
||||
) {
|
||||
setLogoutStates();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionResponse.data && !sessionResponse.data.user.can_access_studio) {
|
||||
updateAuthStatus(AUTH_STATUS.NO_ACCESS);
|
||||
return;
|
||||
}
|
||||
|
||||
const billingApi = new BillingApi();
|
||||
const subscriptionsResponse = await billingApi.ListActiveSubscriptions();
|
||||
if (
|
||||
!subscriptionsResponse.success ||
|
||||
!subscriptionsResponse.data ||
|
||||
!subscriptionsResponse.data.active_subscriptions
|
||||
) {
|
||||
setLogoutStates();
|
||||
return;
|
||||
}
|
||||
|
||||
updateUserInfo(sessionResponse.data.user);
|
||||
updateActiveSubscriptions({
|
||||
maybe_loyalty_program: subscriptionsResponse.data.maybe_loyalty_program,
|
||||
active_subscriptions: subscriptionsResponse.data.active_subscriptions || [],
|
||||
});
|
||||
updateAuthStatus(AUTH_STATUS.LOGGED_IN);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./authentication";
|
||||
export * from "./fetchers";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
import { UserInfo, ActiveSubscriptions } from "~/models";
|
||||
import {
|
||||
authentication,
|
||||
// flushAllBackgroundLoadedMedia
|
||||
} from "~/signals";
|
||||
|
||||
export const updateAuthStatus = (newStatus: AUTH_STATUS) => {
|
||||
authentication.status.value = newStatus;
|
||||
};
|
||||
|
||||
export const updateUserInfo = (
|
||||
newInfo: UserInfo | undefined,
|
||||
flush?: boolean,
|
||||
) => {
|
||||
if (newInfo && !flush) {
|
||||
//case of updating UserInfo partly
|
||||
authentication.userInfo.value = {
|
||||
...authentication.userInfo.value,
|
||||
...newInfo,
|
||||
};
|
||||
} else {
|
||||
//case of setting a new User
|
||||
//case of deleting userInfo
|
||||
authentication.userInfo.value = newInfo;
|
||||
}
|
||||
};
|
||||
export const updateActiveSubscriptions = (
|
||||
activeSubs: ActiveSubscriptions | undefined,
|
||||
) => {
|
||||
authentication.activeSubs.value = activeSubs;
|
||||
};
|
||||
|
||||
// this function is not exposed, only the logout function is
|
||||
export const setLogoutStates = () => {
|
||||
updateAuthStatus(AUTH_STATUS.LOGGED_OUT);
|
||||
updateUserInfo(undefined);
|
||||
updateActiveSubscriptions(undefined);
|
||||
// flushAllBackgroundLoadedMedia();
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./authentication";
|
||||
+20
-1
@@ -18,7 +18,6 @@ export default {
|
||||
// common colors
|
||||
white: colors.white,
|
||||
black: colors.black,
|
||||
gray: colors.slate, //use slate instead of gray
|
||||
red: colors.red,
|
||||
oragne: colors.orange,
|
||||
yellow: colors.yellow,
|
||||
@@ -27,6 +26,13 @@ export default {
|
||||
indigo: colors.indigo,
|
||||
purple: colors.purple,
|
||||
|
||||
// utility colors
|
||||
error: colors.red[500],
|
||||
warning: colors.yellow[500],
|
||||
success: colors.green[500],
|
||||
info: colors.sky[500],
|
||||
|
||||
// brand colors
|
||||
primary: {
|
||||
//sunglo
|
||||
DEFAULT: "#e66462", //=500
|
||||
@@ -72,6 +78,19 @@ export default {
|
||||
900: "#1e505b",
|
||||
950: "#0e353e",
|
||||
},
|
||||
|
||||
ui: {
|
||||
900: "#1a1a27",
|
||||
background: "#1a1a27",
|
||||
700: "#242433",
|
||||
panel: "#242433",
|
||||
500: "#393948",
|
||||
border: "#393948",
|
||||
300: "#515168",
|
||||
divider: "#515168",
|
||||
100: "#676781",
|
||||
button: "#676781",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
Reference in new issue
Block a user