diff --git a/src/app/global.css b/src/app/global.css
index 144756cea2..701408031c 100644
--- a/src/app/global.css
+++ b/src/app/global.css
@@ -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;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/hoc/index.ts b/src/components/hoc/index.ts
new file mode 100644
index 0000000000..625025a76c
--- /dev/null
+++ b/src/components/hoc/index.ts
@@ -0,0 +1 @@
+export * from "./withProtectedRoute";
diff --git a/src/components/hoc/withProtectedRoute.tsx b/src/components/hoc/withProtectedRoute.tsx
new file mode 100644
index 0000000000..3b1da04066
--- /dev/null
+++ b/src/components/hoc/withProtectedRoute.tsx
@@ -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 =
(
+ Component: ComponentType
,
+) =>
+ 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 (
+
+
+
+ );
+ }
+
+ if (
+ status.value === AUTH_STATUS.LOGGED_IN &&
+ userInfo.value &&
+ userInfo.value.can_access_studio
+ ) {
+ return ;
+ }
+
+ return ;
+ };
+
+const RedirectToLogin = () => {
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+ const timeoutTimer = useRef(undefined);
+
+ useEffect(() => {
+ if (!timeoutTimer.current) {
+ timeoutTimer.current = setTimeout(
+ () => navigate(`/login?redirect=${pathname}`),
+ 2000,
+ );
+ }
+ }, [navigate, pathname]);
+ return (
+
+
+
+ );
+};
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
new file mode 100644
index 0000000000..eff29707a5
--- /dev/null
+++ b/src/components/ui/index.ts
@@ -0,0 +1 @@
+export * from "./spinner";
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000000..8874f60e61
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/src/enums/Authentication.ts b/src/enums/Authentication.ts
new file mode 100644
index 0000000000..ce32954d6a
--- /dev/null
+++ b/src/enums/Authentication.ts
@@ -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",
+}
diff --git a/src/enums/Billing.ts b/src/enums/Billing.ts
new file mode 100644
index 0000000000..c602de1b8a
--- /dev/null
+++ b/src/enums/Billing.ts
@@ -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",
+}
diff --git a/src/enums/Users.ts b/src/enums/Users.ts
new file mode 100644
index 0000000000..76c4b77ae8
--- /dev/null
+++ b/src/enums/Users.ts
@@ -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",
+}
diff --git a/src/enums/index.ts b/src/enums/index.ts
new file mode 100644
index 0000000000..1086d43982
--- /dev/null
+++ b/src/enums/index.ts
@@ -0,0 +1,3 @@
+export * from "./Authentication";
+export * from "./Billing";
+export * from "./Users";
diff --git a/src/models/Billing.tsx b/src/models/Billing.tsx
new file mode 100644
index 0000000000..fc832816d2
--- /dev/null
+++ b/src/models/Billing.tsx
@@ -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;
+}
diff --git a/src/models/Users.ts b/src/models/Users.ts
new file mode 100644
index 0000000000..fc0caf6f17
--- /dev/null
+++ b/src/models/Users.ts
@@ -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;
+}
diff --git a/src/models/index.ts b/src/models/index.ts
new file mode 100644
index 0000000000..557be9b1ae
--- /dev/null
+++ b/src/models/index.ts
@@ -0,0 +1,2 @@
+export * from "./Billing";
+export * from "./Users";
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index 8624f38632..6ee12ce441 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -1,3 +1,10 @@
-export const Main = () => {
- return Main
;
-};
+import { withProtectionRoute } from "~/components/hoc";
+import { authentication } from "~/signals";
+
+export const Main = withProtectionRoute(() => {
+ return (
+
+
you are logged in as {authentication.userInfo.value?.display_name}
+
+ );
+});
diff --git a/src/signals/authentication/authentication.ts b/src/signals/authentication/authentication.ts
new file mode 100644
index 0000000000..b7a1128225
--- /dev/null
+++ b/src/signals/authentication/authentication.ts
@@ -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.INIT);
+const userInfo = signal(undefined);
+const activeSubs = signal(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,
+};
diff --git a/src/signals/authentication/fetchers.ts b/src/signals/authentication/fetchers.ts
new file mode 100644
index 0000000000..a46f2c4372
--- /dev/null
+++ b/src/signals/authentication/fetchers.ts
@@ -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);
+}
diff --git a/src/signals/authentication/index.ts b/src/signals/authentication/index.ts
new file mode 100644
index 0000000000..bf6a51e48f
--- /dev/null
+++ b/src/signals/authentication/index.ts
@@ -0,0 +1,2 @@
+export * from "./authentication";
+export * from "./fetchers";
diff --git a/src/signals/authentication/utilities.ts b/src/signals/authentication/utilities.ts
new file mode 100644
index 0000000000..b8845db13c
--- /dev/null
+++ b/src/signals/authentication/utilities.ts
@@ -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();
+};
diff --git a/src/signals/index.ts b/src/signals/index.ts
new file mode 100644
index 0000000000..947dc27329
--- /dev/null
+++ b/src/signals/index.ts
@@ -0,0 +1 @@
+export * from "./authentication";
diff --git a/tailwind.config.js b/tailwind.config.js
index 10838a1cbc..58efac081f 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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: [],