authentication signals and auto login

This commit is contained in:
wil committed 2024-08-28 22:38:28 -04:00
1 parent 43ab6a88a7
commit a52013fd0e
19 files changed
+440 -10

No files matched your search

+13 -6
View File
@@ -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;
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from "./withProtectedRoute";
+59
View File
@@ -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>
);
};
+1
View File
@@ -0,0 +1 @@
export * from "./spinner";
+14
View File
@@ -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>
);
};
+15
View File
@@ -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",
}
+11
View File
@@ -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",
}
+6
View File
@@ -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",
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./Authentication";
export * from "./Billing";
export * from "./Users";
+17
View File
@@ -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;
}
+50
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./Billing";
export * from "./Users";
+10 -3
View File
@@ -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,
};
+127
View File
@@ -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);
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./authentication";
export * from "./fetchers";
+40
View File
@@ -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();
};
+1
View File
@@ -0,0 +1 @@
export * from "./authentication";
+20 -1
View File
@@ -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: [],