Compare commits

...
Author SHA1 Message Date
Brandon Thomas d1e7c6ab15 cleanup packages 2025-09-24 01:19:55 -04:00
Brandon Thomas 760a329c89 cleanup 2025-09-24 01:17:13 -04:00
Brandon Thomas b2d11a351a signup source working 2025-09-24 01:08:08 -04:00
Brandon Thomas 25fb4b6b50 attempted frontend changes - too many API copies 2025-09-24 00:45:32 -04:00
Brandon Thomas 68cb6e4f59 endpoint with signup source 2025-09-24 00:10:06 -04:00
24 changed files with 758 additions and 609 deletions
@@ -186,8 +186,8 @@ CREATE TABLE users (
-- ========== TRACKING ==========
-- Where the user signed up from (NOT NECESSARILY AN ENUM!)
-- For now this will be "fakeyou" and "storyteller", but we may extend
-- or overload this to handle other cases or metadata.
-- For now this will be "artcraft", "fakeyou", or "storyteller",
-- though we may extend or overload this to handle other cases or metadata.
maybe_source VARCHAR(255) DEFAULT NULL,
-- How users created their account
@@ -3,11 +3,12 @@
#![forbid(unused_mut)]
#![forbid(unused_variables)]
use sqlx::MySqlPool;
use enums::by_table::users::user_signup_method::UserSignupMethod;
use crate::queries::users::user::create::create_account_error::CreateAccountError;
use crate::queries::users::user::create::create_account_generic::{create_account_generic, GenericCreateAccountArgs};
use crate::utils::transactor::Transactor;
use enums::by_table::users::user_signup_method::UserSignupMethod;
use enums::by_table::users::user_signup_source::UserSignupSource;
use sqlx::MySqlPool;
use tokens::tokens::users::UserToken;
pub struct CreateAccountFromEmailPasswordArgs<'a> {
@@ -17,7 +18,7 @@ pub struct CreateAccountFromEmailPasswordArgs<'a> {
pub email_gravatar_hash: &'a str,
pub password_hash: &'a str,
pub ip_address: &'a str,
pub maybe_source: Option<&'a str>,
pub maybe_source: Option<UserSignupSource>,
/// In production code, send this as `None`.
/// Only provide an external user token for db integration tests and db seeding tools.
@@ -7,6 +7,7 @@ use crate::queries::users::user::create::create_account_error::CreateAccountErro
use crate::queries::users::user::create::create_account_generic::{create_account_generic, GenericCreateAccountArgs};
use crate::utils::transactor::Transactor;
use enums::by_table::users::user_signup_method::UserSignupMethod;
use enums::by_table::users::user_signup_source::UserSignupSource;
use tokens::tokens::users::UserToken;
/// SSO accounts do not have passwords at account creation
@@ -22,7 +23,7 @@ pub struct CreateAccountFromGoogleSsoArgs<'a> {
pub email_confirmed_by_google: bool,
pub ip_address: &'a str,
pub maybe_source: Option<&'a str>,
pub maybe_source: Option<UserSignupSource>,
/// Comma separated string of feature flags.
pub maybe_feature_flags: Option<&'a str>,
@@ -5,9 +5,10 @@
use crate::queries::users::user::create::create_account_error::CreateAccountError;
use crate::utils::transactor::Transactor;
use enums::by_table::users::user_signup_method::UserSignupMethod;
use enums::by_table::users::user_signup_source::UserSignupSource;
use log::warn;
use sqlx::error::Error::Database;
use enums::by_table::users::user_signup_method::UserSignupMethod;
use tokens::tokens::users::UserToken;
pub struct GenericCreateAccountArgs<'a> {
@@ -28,7 +29,7 @@ pub struct GenericCreateAccountArgs<'a> {
pub is_without_password: bool,
pub ip_address: &'a str,
pub maybe_source: Option<&'a str>,
pub maybe_source: Option<UserSignupSource>,
pub maybe_signup_method: Option<UserSignupMethod>,
/// Comma separated string of feature flags.
@@ -119,7 +120,7 @@ SET
args.ip_address,
args.ip_address,
args.maybe_source,
args.maybe_source.map(|s| s.to_str()),
args.maybe_signup_method.map(|m| m.to_str()),
);
@@ -1,2 +1,3 @@
pub mod user_feature_flag;
pub mod user_signup_method;
pub mod user_signup_method;
pub mod user_signup_source;
@@ -0,0 +1,138 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
use utoipa::ToSchema;
/// Used in the `users` table in a `VARCHAR(255)` (which should be a `VARCHAR(16)`) field, `maybe_source`.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum UserSignupSource {
#[serde(rename = "artcraft")]
ArtCraft,
#[serde(rename = "fakeyou")]
FakeYou,
#[serde(rename = "storyteller")]
Storyteller,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(UserSignupSource);
impl_mysql_enum_coders!(UserSignupSource);
impl_mysql_from_row!(UserSignupSource);
/// NB: Legacy API for older code.
impl UserSignupSource {
pub fn to_str(&self) -> &'static str {
match self {
Self::ArtCraft => "artcraft",
Self::FakeYou => "fakeyou",
Self::Storyteller => "storyteller",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"artcraft" => Ok(Self::ArtCraft),
"fakeyou" => Ok(Self::FakeYou),
"storyteller" => Ok(Self::Storyteller),
_ => Err(format!("invalid value: {:?}", value)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::ArtCraft,
Self::FakeYou,
Self::Storyteller,
])
}
}
#[cfg(test)]
mod tests {
use crate::by_table::users::user_signup_source::UserSignupSource;
use crate::test_helpers::assert_serialization;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(UserSignupSource::ArtCraft, "artcraft");
assert_serialization(UserSignupSource::FakeYou, "fakeyou");
assert_serialization(UserSignupSource::Storyteller, "storyteller");
}
}
mod impl_methods {
use super::*;
#[test]
fn to_str() {
assert_eq!(UserSignupSource::ArtCraft.to_str(), "artcraft");
assert_eq!(UserSignupSource::FakeYou.to_str(), "fakeyou");
assert_eq!(UserSignupSource::Storyteller.to_str(), "storyteller");
}
#[test]
fn from_str() {
assert_eq!(UserSignupSource::from_str("artcraft").unwrap(), UserSignupSource::ArtCraft);
assert_eq!(UserSignupSource::from_str("fakeyou").unwrap(), UserSignupSource::FakeYou);
assert_eq!(UserSignupSource::from_str("storyteller").unwrap(), UserSignupSource::Storyteller);
assert!(UserSignupSource::from_str("foo").is_err());
}
}
mod manual_variant_checks {
use super::*;
#[test]
fn all_variants() {
let mut variants = UserSignupSource::all_variants();
assert_eq!(variants.len(), 3);
assert_eq!(variants.pop_first(), Some(UserSignupSource::ArtCraft));
assert_eq!(variants.pop_first(), Some(UserSignupSource::FakeYou));
assert_eq!(variants.pop_first(), Some(UserSignupSource::Storyteller));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(UserSignupSource::all_variants().len(), UserSignupSource::iter().len());
}
#[test]
fn round_trip() {
for variant in UserSignupSource::all_variants() {
assert_eq!(variant, UserSignupSource::from_str(variant.to_str()).unwrap());
assert_eq!(variant, UserSignupSource::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, UserSignupSource::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 16;
for variant in UserSignupSource::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
}
}
}
}
@@ -7,7 +7,7 @@ use std::collections::HashMap;
use std::fmt;
use std::fmt::Formatter;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source_enum;
use crate::http_server::session::http::http_user_session_manager::HttpUserSessionManager;
use crate::http_server::validations::is_reserved_username::is_reserved_username;
use crate::http_server::validations::validate_passwords::validate_passwords;
@@ -17,6 +17,7 @@ use crate::util::enroll_in_studio::enroll_in_studio;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::{web, HttpRequest, HttpResponse};
use enums::by_table::users::user_signup_source::UserSignupSource;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use log::{info, warn};
@@ -36,6 +37,10 @@ pub struct CreateAccountRequest {
pub password: String,
pub password_confirmation: String,
pub email_address: String,
/// Optional: Source of the signup, e.g. "artcraft", "fakeyou", "storyteller", etc.
/// If not provided, we try to infer it from the Origin header instead.
pub signup_source: Option<UserSignupSource>,
}
#[derive(ToSchema, Serialize)]
@@ -184,7 +189,11 @@ pub async fn create_account_handler(
let ip_address = get_request_ip(&http_request);
let maybe_source = get_request_signup_source(&http_request);
let mut maybe_source = request.signup_source;
if maybe_source.is_none() {
maybe_source = get_request_signup_source_enum(&http_request);
}
let create_account_result = create_account_from_email_and_password(
&mysql_pool,
@@ -1,6 +1,6 @@
use crate::http_server::endpoints::users::google_sso::google_sso_handler::GoogleCreateAccountErrorResponse;
use crate::http_server::endpoints::users::google_sso::handle_new_sso_account::NewSsoAccountInfo;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source;
use crate::http_server::requests::get_request_signup_source::{get_request_signup_source, get_request_signup_source_enum};
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::util::email_to_gravatar::email_to_gravatar;
use crate::util::generate_random_username::generate_random_username;
@@ -42,7 +42,7 @@ pub async fn handle_new_sso_account_for_new_user(
let ip_address = get_request_ip(&args.http_request);
let user_email_gravatar_hash = email_to_gravatar(&args.user_email_address);
let mut maybe_source = get_request_signup_source(&args.http_request);
let mut maybe_source = get_request_signup_source_enum(&args.http_request);
let mut maybe_user_token = None;
let mut maybe_user_display_name = None;
@@ -1,7 +1,9 @@
use actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
use actix_web::HttpRequest;
use enums::by_table::users::user_signup_source::UserSignupSource;
use log::warn;
const ARTCRAFT : &str = "artcraft";
const FAKEYOU : &str = "fakeyou";
const STORYTELLER : &str = "storyteller";
@@ -25,7 +27,9 @@ pub fn get_request_signup_source(http_request: &HttpRequest) -> Option<&'static
match maybe_origin {
Ok(Some(uri)) => {
if let Some(host) = uri.host() {
if host.contains(STORYTELLER) {
if host.contains(ARTCRAFT) {
return Some(ARTCRAFT);
} else if host.contains(STORYTELLER) {
return Some(STORYTELLER);
} else if host.contains(FAKEYOU) {
return Some(FAKEYOU);
@@ -46,13 +50,42 @@ pub fn get_request_signup_source(http_request: &HttpRequest) -> Option<&'static
None
}
pub fn get_request_signup_source_enum(http_request: &HttpRequest) -> Option<UserSignupSource> {
get_request_signup_source(http_request)
.and_then(|source_str| UserSignupSource::from_str(source_str).ok())
}
#[cfg(test)]
mod tests {
use crate::http_server::requests::get_request_signup_source::get_request_signup_source;
use actix_web::test::TestRequest;
mod origin_header {
mod origin_header_str {
use super::*;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source;
#[test]
fn artcraft_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://artcraft.ai"))
.to_http_request();
assert_eq!(get_request_signup_source(&request), Some("artcraft"));
}
#[test]
fn get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source(&request), Some("artcraft"));
}
#[test]
fn api_dot_get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source(&request), Some("artcraft"));
}
#[test]
fn fakeyou_dot_com() {
@@ -86,4 +119,66 @@ mod tests {
assert_eq!(get_request_signup_source(&request), Some("storyteller"));
}
}
mod origin_header_enum {
use super::*;
use crate::http_server::requests::get_request_signup_source::get_request_signup_source_enum;
use enums::by_table::users::user_signup_source::UserSignupSource;
#[test]
fn artcraft_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://artcraft.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn api_dot_get_artcraft_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.getartcraft.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::ArtCraft));
}
#[test]
fn fakeyou_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://fakeyou.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::FakeYou));
}
#[test]
fn api_dot_fakeyou_dot_com() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.fakeyou.com"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::FakeYou));
}
#[test]
fn storyteller_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://storyteller.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::Storyteller));
}
#[test]
fn api_dot_storyteller_dot_ai() {
let request = TestRequest::get()
.insert_header(("origin", "https://api.storyteller.ai"))
.to_http_request();
assert_eq!(get_request_signup_source_enum(&request), Some(UserSignupSource::Storyteller));
}
}
}
-11
View File
@@ -1,11 +0,0 @@
BASE_API="https://api.storyteller.ai"
GOOGLE_API="https://storage.googleapis.com"
FUNNEL_API="https://style.storyteller.ai"
CDN_API="https://cdn.storyteller.ai"
GRAVATAR_API="https://www.gravatar.com"
DEPLOY_PRIME_URL="https://studio-staging.studio.storyteller.ai"
REACT_APP_PUBLIC_POSTHOG_KEY="phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
REACT_APP_PUBLIC_POSTHOG_UI="https://us.i.posthog.com"
DEPLOY_CONTEXT="DEVELOPMENT"
CONTEXT="DEVELOPMENT"
UPLOAD_API_VIDEO="https://upload.storyteller.ai"
-16
View File
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Login | ArtCraft 3D</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
</head>
<body className="overflow-hidden bg-ui-background">
<div id="root"></div>
<script type="module" src="/src/login.tsx"></script>
</body>
</html>
-16
View File
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Signup | ArtCraft 3D</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
</head>
<body className="overflow-hidden bg-ui-background">
<div id="root"></div>
<script type="module" src="/src/signup.tsx"></script>
</body>
</html>
@@ -1,6 +1,14 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { UserInfo } from "~/models";
interface SignupRequest {
username: string;
email_address: string;
password: string;
password_confirmation: string;
signup_source?: string;
};
export class UsersApi extends ApiManager {
public GetSession(): Promise<
ApiResponse<{
@@ -113,26 +121,26 @@ export class UsersApi extends ApiManager {
email,
password,
passwordConfirmation,
signupSource,
}: {
username: string;
email: string;
password: string;
passwordConfirmation: string;
signupSource?: string;
}): Promise<ApiResponse<{ signedSession?: string }>> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/create_account`;
const body = {
let body: SignupRequest = {
email_address: email,
password,
password_confirmation: passwordConfirmation,
username,
};
if (!!signupSource) {
body.signup_source = signupSource;
}
return await this.post<
{
username: string;
email_address: string;
password: string;
password_confirmation: string;
},
SignupRequest,
{
success: boolean;
signed_session?: string;
-203
View File
@@ -1,203 +0,0 @@
import { useRef, StrictMode } from "react";
import { useNavigate, useSearchParams, BrowserRouter } from "react-router-dom";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { twMerge } from "tailwind-merge";
import {
faKey,
faSpinnerThird,
faUser,
} from "@fortawesome/pro-solid-svg-icons";
import { AUTH_STATUS } from "~/enums";
import { authentication, login, logout } from "~/signals";
import { createRoot } from "react-dom/client";
import { ConfirmationModal } from "~/components/reusable/ConfirmationModal";
import { Input } from "@storyteller/ui-input";
import { Button } from "@storyteller/ui-button";
import "./styles/normalize.css";
import "./styles/tailwind.css";
import "./styles/base.css";
import "@fortawesome/fontawesome-svg-core/styles.css";
import { config } from "@fortawesome/fontawesome-svg-core";
import { GlobalSettingsManager } from "./pages/PageEnigma/GlobalSettingsManager";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
config.autoAddCss = false; /* eslint-disable import/first */
// TODO(bt,2025-04-19): Make these configurable
const ENV = {
GOOGLE_API: "https://studio.storyteller.ai",
FUNNEL_API: "https://studio.storyteller.ai",
CDN_API: "https://cdn-2.fakeyou.com",
GRAVATAR_API: "https://studio.storyteller.ai",
DEPLOY_PRIME_URL: "https://studio.storyteller.ai",
};
export default function LoginScreen() {
useSignals();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const formRef = useRef<HTMLFormElement | null>(null);
const { status: authStatus } = authentication;
const authLoaderMessage = getAuthLoaderMessage();
const shouldShowLoader = checkShouldShowLoader();
const showNoAccessModal = authStatus.value === AUTH_STATUS.NO_ACCESS;
const handleOnSumbit = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (formRef.current) {
const form = new FormData(formRef.current);
const usernameOrEmail = form.get("usernameOrEmail")?.toString();
const password = form.get("password")?.toString();
if (usernameOrEmail && password && login) {
login({
usernameOrEmail,
password,
});
}
}
};
useSignalEffect(() => {
const redirectPath = searchParams.get("redirect");
if (authStatus.value === AUTH_STATUS.LOGGED_IN) {
navigate(redirectPath ? redirectPath : "/");
}
});
return (
<div className="fixed flex h-full w-full flex-col items-center justify-center bg-[#1b1b1f] bg-center bg-repeat">
<div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
<img
src="/resources/images/artcraft-logo-3.png"
alt="ArtCraft Logo"
className="h-9 select-none"
/>
</div>
<div className="w-full max-w-xl rounded-xl shadow-lg">
<div className="glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none">
<h1 className="mb-8 text-center text-2xl font-bold">
Log in to 3D Stage Editor
</h1>
<form
ref={formRef}
onSubmit={handleOnSumbit}
className={twMerge(
"flex flex-col gap-4",
shouldShowLoader && "opacity-0",
)}
>
<Input
label="Username or Email"
icon={faUser}
name="usernameOrEmail"
placeholder="Username or Email"
autoComplete="username"
required
/>
<Input
label="Password"
icon={faKey}
type="password"
name="password"
placeholder="Password"
autoComplete="current-password"
required
/>
<Button className="mt-4 py-3">Continue</Button>
</form>
{shouldShowLoader && (
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-black/50">
<div className="flex items-center text-center">
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin"
/>
<p className="text-sm text-white">{authLoaderMessage}</p>
</div>
</div>
)}
</div>
<div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
{/* <a
href="https://storyteller.ai/password-reset"
className="grow text-sm font-medium !text-brand-primary-400 transition-all duration-150 hover:!text-brand-primary-300"
>
Forgot your password?
</a> */}
<a
href="/"
className="grow text-sm font-medium !text-brand-primary-400 transition-all duration-150 hover:!text-brand-primary-300"
>
Back
</a>
<div className="flex justify-end gap-1 text-sm">
<p className="opacity-75">Don&apos;t have an account?</p>
<a
href="/signup"
className="font-medium text-brand-primary-400 hover:text-brand-primary-300"
>
Sign up
</a>
</div>
</div>
</div>
<ConfirmationModal
text="We're in a closed beta and you'll need a beta key to use this app."
title="Unauthorized"
open={showNoAccessModal}
onClose={handleClose}
cancelText="Close"
onCancel={handleClose}
okText="Get Beta Key"
onOk={() => {
handleClose();
if (window) {
window.open("https://storyteller.ai/beta-key/redeem", "_blank");
}
}}
/>
</div>
);
}
const handleClose = () => {
if (authentication.status.value !== AUTH_STATUS.LOGGED_OUT) {
logout();
}
};
const checkShouldShowLoader = () => {
return (
authentication.status.value === AUTH_STATUS.LOGGING ||
authentication.status.value === AUTH_STATUS.LOGGED_IN
);
};
const getAuthLoaderMessage = () => {
if (authentication.status.value === AUTH_STATUS.LOGGED_IN) {
return "Authenticated, Redirecting...";
}
if (authentication.status.value === AUTH_STATUS.GET_USER_INFO) {
return "Getting User Info...";
}
return "Getting Session...";
};
// TODO: Replace environment variables from `root.tsx`
createRoot(document.getElementById("root")!).render(
<>
<StrictMode>
<BrowserRouter>
<GlobalSettingsManager env={ENV} />
<LoginScreen />
</BrowserRouter>
</StrictMode>
</>,
);
@@ -57,20 +57,25 @@ export const signUp = async ({
email,
password,
passwordConfirmation,
signupSource,
}: {
username: string;
email: string;
password: string;
passwordConfirmation: string;
signupSource?: string;
}) => {
updateAuthStatus(AUTH_STATUS.LOGGING);
console.log(">>> Signup with source: ", signupSource);
const usersApi = new UsersApi();
const response = await usersApi.Signup({
email,
password,
passwordConfirmation,
username,
signupSource,
});
console.log(response);
if (!response.success || !response.data) {
-197
View File
@@ -1,197 +0,0 @@
import { faEnvelope, faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
import { GlobalSettingsManager } from "./pages/PageEnigma/GlobalSettingsManager";
import { Input } from "@storyteller/ui-input";
import { Button } from "@storyteller/ui-button";
import { addToast, authentication, signUp } from "~/signals";
import { FormEvent, useRef, useState, StrictMode } from "react";
import { AUTH_STATUS, ToastTypes } from "~/enums";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useNavigate, useSearchParams, BrowserRouter } from "react-router-dom";
import { createRoot } from "react-dom/client";
import "./styles/normalize.css";
import "./styles/tailwind.css";
import "./styles/base.css";
import "@fortawesome/fontawesome-svg-core/styles.css";
import { config } from "@fortawesome/fontawesome-svg-core";
config.autoAddCss = false; /* eslint-disable import/first */
const regEx =
/^(([^<>()[\].,;:\s@"]+(.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
// TODO(bt,2025-04-19): Make these configurable
const ENV = {
GOOGLE_API: "https://studio.storyteller.ai",
FUNNEL_API: "https://studio.storyteller.ai",
CDN_API: "https://cdn-2.fakeyou.com",
GRAVATAR_API: "https://studio.storyteller.ai",
DEPLOY_PRIME_URL: "https://studio.storyteller.ai",
};
export default function SignUpScreen() {
useSignals();
const navigate = useNavigate();
const formRef = useRef<HTMLFormElement | null>(null);
const [usernameError, setUsernameError] = useState("");
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
const [passwordConfirmationError, setPasswordConfirmationError] =
useState("");
const [searchParams] = useSearchParams();
const { status: authStatus } = authentication;
const handleOnSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (formRef.current) {
const form = new FormData(formRef.current);
const email = form.get("email")?.toString();
const password = form.get("password")?.toString();
const passwordConfirmation = form
.get("password-confirmation")
?.toString();
const username = form.get("username")?.toString();
setUsernameError("");
if (!username || username.length < 6) {
setUsernameError("Username must be at least 6 characters long");
}
setEmailError("");
if (!email || !email.match(regEx)) {
setEmailError("Email must be a valid email address");
}
setPasswordError("");
if (!password || password.length < 8) {
setPasswordError("Password must be at least 8 characters long");
}
setPasswordConfirmationError("");
if (!passwordError && passwordConfirmation !== password) {
setPasswordConfirmationError("Passwords do not match");
}
if (
!usernameError &&
!passwordError &&
!emailError &&
!passwordConfirmationError &&
signUp
) {
signUp({
username: username!,
email: email!,
password: password!,
passwordConfirmation: passwordConfirmation!,
}).then((error) => {
console.log("Error occured", error);
if (error) {
addToast(ToastTypes.ERROR, error);
}
});
}
}
}; // end handleOnSubmit
useSignalEffect(() => {
const redirectPath = searchParams.get("redirect");
if (authStatus.value === AUTH_STATUS.LOGGED_IN) {
navigate(redirectPath ? redirectPath : "/");
return;
}
if (authStatus.value === AUTH_STATUS.NO_ACCESS) {
window.open("https://storyteller.ai/", "_self");
return;
}
});
return (
<div className="fixed flex h-full w-full flex-col items-center justify-center overflow-auto bg-[#1b1b1f] bg-center bg-repeat">
<div className="mb-7 flex w-10/12 max-w-2xl items-center justify-center gap-4">
<img
src="/resources/images/artcraft-logo-3.png"
alt="ArtCraft Logo"
className="h-9 select-none"
/>
</div>
<div className="w-full max-w-xl rounded-xl shadow-lg">
<div className="glass glass-no-hover relative mx-auto w-full rounded-none rounded-t-xl p-6 shadow-none">
<h1 className="mb-8 text-center text-2xl font-bold">Sign up</h1>
<form
ref={formRef}
onSubmit={handleOnSubmit}
className="flex flex-col gap-4"
>
<Input
label="Username"
icon={faUser}
placeholder="Username"
name="username"
errorMessage={usernameError}
required
/>
<Input
type="email"
label="Email"
icon={faEnvelope}
placeholder="Email"
name="email"
errorMessage={emailError}
required
/>
<Input
type="password"
label="Password"
icon={faKey}
name="password"
placeholder="Password"
autoComplete="current-password"
errorMessage={passwordError}
required
/>
<Input
type="password"
label="Password Confirmation"
icon={faKey}
placeholder="Password Confirmation"
name="password-confirmation"
errorMessage={passwordConfirmationError}
required
/>
<Button className="mt-4 py-3">Sign up</Button>
</form>
</div>
<div className="align-items flex w-full max-w-xl rounded-b-xl bg-ui-controls px-6 py-4">
<div className="flex w-full justify-start gap-1 text-sm">
<p className="opacity-75"></p>
<a
href="/"
className="font-medium text-brand-primary-400 hover:text-brand-primary-300"
>
Back
</a>
</div>
<div className="flex w-full justify-end gap-1 text-sm">
<p className="opacity-75">Already have an account?</p>
<a
href="/login"
className="font-medium text-brand-primary-400 hover:text-brand-primary-300"
>
Log in
</a>
</div>
</div>
</div>
</div>
);
}
// TODO: Replace environment variables from `root.tsx`
createRoot(document.getElementById("root")!).render(
<>
<StrictMode>
<BrowserRouter>
<GlobalSettingsManager env={ENV} />
<SignUpScreen />
</BrowserRouter>
</StrictMode>
</>,
);
-69
View File
@@ -1,69 +0,0 @@
#!/bin/bash
# NB: This file is executed by Netlify to build the website
# Verbose printing, exit on failure.
set -euxo pipefail
# Echo env vars
env
# Add the GIT SHA to the build
# This must be done before everything else, or it will get cached with the build.
# (This might be making the builds less performant?)
# COMMIT_REF is defined by Netlify to be the commit SHA
# We want a short 8 character reference.
echo "Labelling build with short SHA..."
SHORT_SHA=$(echo "${COMMIT_REF}" | cut -c1-8)
echo "Baking current git SHA to code";
find . -type f -exec sed -i "s/%CURRENT_STORYTELLER_GIT_VERSION%/${SHORT_SHA}/g" {} +
####
# Expressions hack
####
echo "Baking expressions feature flag ENV to code";
echo "Value of EXPRESSIONS env var is: ${EXPRESSIONS}"
if [ "$EXPRESSIONS" = "TRUE" ] || [ "$EXPRESSIONS" = "true" ] ; then
expressions_value="true"
else
expressions_value="false"
fi
echo "Value of expressions is: ${expressions_value}"
find . -type f -exec sed -i "s/%EXPRESSIONS_VALUE%/${expressions_value}/g" {} +
echo "Baking URL/API ENV configs to code";
# NB: We can't use slashes as the sed escape character:
# https://stackoverflow.com/a/27787551
d=$'\03'
find . -type f -exec sed -i "s${d}%BUILD_BASE_API%${d}${BASE_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%BUILD_GOOGLE_API%${d}${GOOGLE_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%BUILD_FUNNEL_API%${d}${FUNNEL_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%BUILD_CDN_API%${d}${CDN_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%BUILD_UPLOAD_VIDEO%${d}${UPLOAD_VIDEO_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%BUILD_MEDIA_VIDEO%${d}${MEDIA_VIDEO_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%BUILD_GRAVATAR_VIDEO%${d}${GRAVATAR_API}${d}g" {} +
find . -type f -exec sed -i "s${d}%DEPLOY_PRIME_URL%${d}${DEPLOY_PRIME_URL}${d}g" {} +
find . -type f -exec sed -i "s${d}%DEPLOY_CONTEXT%${d}${DEPLOY_CONTEXT}${d}g" {} +
find . -type f -exec sed -i "s${d}%CONTEXT%${d}${CONTEXT}${d}g" {} +
find . -type f -exec sed -i "s${d}%UPLOAD_API_VIDEO%${d}${UPLOAD_API_VIDEO}${d}g" {} +
# Replace Posthog env variables in the code
find . -type f -exec sed -i "s${d}%REACT_APP_PUBLIC_POSTHOG_KEY%${d}${REACT_APP_PUBLIC_POSTHOG_KEY}${d}g" {} +
find . -type f -exec sed -i "s${d}%REACT_APP_PUBLIC_POSTHOG_UI%${d}${REACT_APP_PUBLIC_POSTHOG_UI}${d}g" {} +
# TODO: Run tests when we add them.
# Ensure the environment variables are exported
export REACT_APP_PUBLIC_POSTHOG_KEY
export REACT_APP_PUBLIC_POSTHOG_UI
export CONTEXT
# Run build.x
npm run build
-4
View File
@@ -19,7 +19,6 @@
"dev": "echo 'editor3d' && vite dev",
"staging-build": "vite build",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint playground/* .",
"start": "netlify serve",
"typecheck": "tsc",
"update:icons": "npm update @awesome.me/kit-fde2be5eb0",
"chromatic": "npx chromatic --project-token=chpt_0945f90d5d3626c",
@@ -34,9 +33,6 @@
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mediapipe/tasks-vision": "^0.10.20",
"@netlify/edge-functions": "^2.3.1",
"@netlify/remix-edge-adapter": "^3.2.0",
"@netlify/remix-runtime": "^2.2.0",
"@preact/signals-react": "^2.0.1",
"@radix-ui/react-tooltip": "^1.2.7",
"@remix-run/deno": "^2.8.1",
-2
View File
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
#!/usr/bin/env bash
set -euxo pipefail
# Kill any process running on port 5741, which will block startup
@@ -1,31 +0,0 @@
#!/bin/bash
set -euxo pipefail
# Add the GIT SHA to the build
# This must be done before everything else, or it will get cached with the build.
# (This might be making the builds less performant?)
# COMMIT_REF is defined by Netlify to be the commit SHA
# We want a short 8 character reference.
echo "Labelling build with short SHA..."
SHORT_SHA=$(echo "${COMMIT_REF}" | cut -c1-8)
echo "Baking current git SHA to code";
find . -type f -exec sed -i "s/%CURRENT_STORYTELLER_GIT_VERSION%/${SHORT_SHA}/g" {} +
echo "Install packages"
npm install
echo "Build Editor 3D"
nx build editor3d --verbose
echo "Change to project dir"
pushd apps/editor3d/
echo "Copy files to putput directory"
cp _headers dist/
#cp _redirects dist/
#cp "404.html" dist/
echo "List final files in build"
find dist/
-4
View File
@@ -1,6 +1,5 @@
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { netlifyPlugin } from "@netlify/remix-edge-adapter/plugin";
import path from 'path'
import { resolve } from 'node:path'
@@ -15,13 +14,10 @@ export default defineConfig({
rollupOptions: {
input: {
index: resolve(__dirname, 'app/index.html'),
login: resolve(__dirname, 'app/login.html'),
signup: resolve(__dirname, 'app/signup.html'),
},
},
},
plugins: [
netlifyPlugin(),
tsconfigPaths(),
],
server: {
+15 -7
View File
@@ -2,6 +2,14 @@ import { ApiManager, ApiResponse } from "./ApiManager.js";
import { UserInfo } from "./models/Users.js";
import { FetchProxy as fetch } from "@storyteller/tauri-utils";
interface SignupRequest {
username: string;
email_address: string;
password: string;
password_confirmation: string;
signup_source?: string;
};
export class UsersApi extends ApiManager {
private async authFetch<B, T>(
endpoint: string,
@@ -149,28 +157,28 @@ export class UsersApi extends ApiManager {
email,
password,
passwordConfirmation,
signupSource,
}: {
username: string;
email: string;
password: string;
passwordConfirmation: string;
signupSource?: string;
}): Promise<ApiResponse<{ signedSession?: string }>> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/create_account`;
const body = {
const body : SignupRequest = {
email_address: email,
password,
password_confirmation: passwordConfirmation,
username,
};
if (!!signupSource) {
body.signup_source = signupSource;
}
try {
const response = await this.authFetch<
{
username: string;
email_address: string;
password: string;
password_confirmation: string;
},
SignupRequest,
{
success: boolean;
signed_session?: string;
@@ -11,6 +11,9 @@ import {
import { ArtCraftSignUp } from "./artcraft-signup";
import { UsersApi } from "@storyteller/api";
import { useLoginModalStore } from "./useLoginModalStore";
const SIGNUP_SOURCE_ARTCRAFT = "artcraft";
interface LoginModalProps {
onClose?: () => void;
videoSrc2D?: string;
@@ -249,6 +252,7 @@ export function LoginModal({
email,
password,
passwordConfirmation,
signupSource: SIGNUP_SOURCE_ARTCRAFT,
});
console.log(signupResponse);
+456 -25
View File
File diff suppressed because it is too large Load Diff