squashing commits

This commit is contained in:
Martina 2021-06-08 15:53:30 -07:00
parent 91ea4d9d9c
commit e8e1e1f26e
105 changed files with 7202 additions and 3084 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

View File

@ -87,6 +87,35 @@ export const checkUsername = async (data) => {
});
};
export const checkEmail = async (data) => {
return await returnJSON(`/api/users/check-email`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
//NOTE(toast): this functionality comes with the upgraded sg plan
// export const validateEmail = async (data) => {
// return await returnJSON("/api/emails/validate", {
// ...DEFAULT_OPTIONS,
// body: JSON.stringify({ data }),
// });
// };
export const sendEmail = async (data) => {
return await returnJSON("/api/emails/send-email", {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const sendTemplateEmail = async (data) => {
return await returnJSON("/api/emails/send-template", {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const archive = async (data) => {
await Websockets.checkWebsocket();
return await returnJSON(`/api/data/archive`, {
@ -188,6 +217,33 @@ export const addFileToSlate = async (data) => {
});
};
export const requestTwitterToken = async () => {
return await returnJSON(`/api/twitter/request-token`, {
...DEFAULT_OPTIONS,
});
};
export const authenticateViaTwitter = async (data) => {
return await returnJSON(`/api/twitter/authenticate`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const createUserViaTwitter = async (data) => {
return await returnJSON(`/api/twitter/signup`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const createUserViaTwitterWithVerification = async (data) => {
return await returnJSON(`/api/twitter/signup-with-verification`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const updateViewer = async (data) => {
await Websockets.checkWebsocket();
return await returnJSON(`/api/users/update`, {
@ -382,62 +438,139 @@ export const getZipFilePaths = async (data) => {
};
export const cleanDatabase = async () => {
return await returnJSON(`api/clean-up/users`, {
return await returnJSON(`/api/clean-up/users`, {
...DEFAULT_OPTIONS,
});
};
export const v1GetSlate = async (data) => {
return await returnJSON(`api/v1/get-slate`, {
return await returnJSON(`/api/v1/get-slate`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v1Get = async (data) => {
return await returnJSON(`api/v1/get`, {
return await returnJSON(`/api/v1/get`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v1UpdateSlate = async (data) => {
return await returnJSON(`api/v1/update-slate`, {
return await returnJSON(`/api/v1/update-slate`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v2GetSlate = async (data) => {
return await returnJSON(`api/v2/get-slate`, {
return await returnJSON(`/api/v2/get-slate`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v2GetUser = async (data) => {
return await returnJSON(`api/v2/get-user`, {
return await returnJSON(`/api/v2/get-user`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v2Get = async (data) => {
return await returnJSON(`api/v2/get`, {
return await returnJSON(`/api/v2/get`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v2UpdateSlate = async (data) => {
return await returnJSON(`api/v2/update-slate`, {
return await returnJSON(`/api/v2/update-slate`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const v2UpdateFile = async (data) => {
return await returnJSON(`api/v2/update-file`, {
return await returnJSON(`/api/v2/update-file`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const createTwitterEmailVerification = async (data) => {
return await returnJSON(`/api/verifications/twitter/create`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const verifyTwitterEmail = async (data) => {
return await returnJSON(`/api/verifications/twitter/verify`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const createPasswordResetVerification = async (data) => {
return await returnJSON(`/api/verifications/password-reset/create`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const verifyPasswordResetEmail = async (data) => {
return await returnJSON(`/api/verifications/password-reset/verify`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const resetPassword = async (data) => {
return await returnJSON(`/api/users/reset-password`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const createLegacyVerification = async (data) => {
return await returnJSON(`/api/verifications/legacy/create`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const migrateUser = async (data) => {
return await returnJSON(`/api/users/migrate`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const createVerification = async (data) => {
return await returnJSON(`/api/verifications/create`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const verifyEmail = async (data) => {
return await returnJSON(`/api/verifications/verify`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const resendVerification = async (data) => {
return await returnJSON(`/api/verifications/resend`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const getUserVersion = async (data) => {
return await returnJSON(`/api/users/get-version`, {
...API_OPTIONS,
body: JSON.stringify({ data }),
});

View File

@ -32,21 +32,21 @@ export const system = {
newYellow: "#F2B256",
newRed: "#BE5234",
shadow: "rgba(15, 14, 18, 0.03)",
blue: "#0061BB",
green: "#006837",
blue: "#0084FF",
green: "#34D159",
yellow: "#FAB413",
red: "#C71313",
red: "#FF4530",
black: "#0F0E12",
newBlack: "#000000",
bgGrayLight: "#F8F8F8",
bgGrayLight: "#E5E5EA",
bgGray: "#F2F2F2",
bgBlue: "#C0D8EE",
bgGreen: "#C0DACD",
bgYellow: "#FEEDC4",
bgRed: "#F1C4C4",
textGray: "#878688",
textGray: "#8E8E93",
textGrayLight: "#C3C3C4",
textGrayDark: "#4B4A4D",
textGrayDark: "#48484A",
textBlack: "#0F0E12",
gray80: "#4B4A4D",
gray70: "#868688",
@ -61,6 +61,7 @@ export const system = {
active: "#00BB00",
blurBlack: "#262626",
bgBlurGray: "#403F42",
bgBlurWhiteTRN: "rgba(255,255,255,0.3)",
grayLight2: "#AEAEB2",
};

View File

@ -4,6 +4,8 @@ export const dispatchCustomEvent = ({ name, detail }) => {
};
export const hasError = (response) => {
console.log("insdie has error");
console.log(response);
if (!response) {
dispatchCustomEvent({
name: "create-alert",

187
common/hooks.js Normal file
View File

@ -0,0 +1,187 @@
import * as React from "react";
import * as Events from "~/common/custom-events";
export const useMounted = () => {
const isMounted = React.useRef(true);
React.useLayoutEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted.current;
};
/** NOTE(amine):
* useForm handles three main responsabilities
* - control inputs
* - control form
* - add validations
*
* For validations
* - Validate each field when onBlur event is triggered
* - Validate all fields before submit
* - font submit if there is errors
*/
export const useForm = ({
onSubmit,
validate,
initialValues,
validateOnBlur = true,
validateOnSubmit = true,
}) => {
const [state, setState] = React.useState({
isSubmitting: false,
values: initialValues,
errors: {},
touched: {},
});
const _hasError = (obj) => Object.keys(obj).some((name) => obj[name]);
const _mergeEventHandlers = (events = []) => (e) =>
events.forEach((event) => {
if (event) event(e);
});
/** ---------- NOTE(amine): Input Handlers ---------- */
const handleFieldChange = (e) =>
setState((prev) => ({
...prev,
values: { ...prev.values, [e.target.name]: e.target.value },
errors: { ...prev.errors, [e.target.name]: undefined },
touched: { ...prev.touched, [e.target.name]: false },
}));
const handleOnBlur = (e) => {
// NOTE(amine): validate the inputs onBlur and touch the current input
let errors = {};
if (validateOnBlur && validate) errors = validate(state.values, { ...state.errors });
setState((prev) => ({
...prev,
touched: { ...prev.touched, [e.target.name]: validateOnBlur },
errors,
}));
};
// Note(Amine): this prop getter will capture the field state
const getFieldProps = (name, { onChange, onBlur, error } = {}) => ({
name: name,
value: state.values[name],
error: error || state.errors[name],
touched: state.touched[name],
onChange: _mergeEventHandlers([onChange, handleFieldChange]),
onBlur: _mergeEventHandlers([onBlur, handleOnBlur]),
});
/** ---------- NOTE(amine): Form Handlers ---------- */
const handleFormOnSubmit = (e) => {
e.preventDefault();
//NOTE(amine): touch all inputs
setState((prev) => {
const touched = Object.keys(prev.values).reduce((acc, key) => ({ ...acc, [key]: true }), {});
return { ...prev, touched };
});
// NOTE(amine): validate inputs
if (validateOnSubmit && validate) {
const errors = validate(state.values, { ...state.errors });
setState((prev) => ({ ...prev, errors }));
if (_hasError(errors)) return;
}
// NOTE(amine): submit the form
if (!onSubmit) return;
setState((prev) => ({ ...prev, isSubmitting: true }));
onSubmit(state.values)
.then(() => {
setState((prev) => ({ ...prev, isSubmitting: false }));
})
.catch(() => {
setState((prev) => ({ ...prev, isSubmitting: false }));
});
};
// Note(Amine): this prop getter will overide the form onSubmit handler
const getFormProps = () => ({
onSubmit: handleFormOnSubmit,
});
return { getFieldProps, getFormProps, values: state.values, isSubmitting: state.isSubmitting };
};
/** NOTE(amine): Since we can use on our design system an input onSubmit,
* useField is a special case of useForm
*/
export const useField = ({
onSubmit,
validate,
initialValue,
onChange,
onBlur,
validateOnBlur = true,
validateOnSubmit = true,
}) => {
const [state, setState] = React.useState({
isSubmitting: false,
value: initialValue,
error: undefined,
touched: undefined,
});
const _mergeEventHandlers = (events = []) => (e) =>
events.forEach((event) => {
if (event) event(e);
});
/** ---------- NOTE(amine): Input Handlers ---------- */
const handleFieldChange = (e) =>
setState((prev) => ({
...prev,
value: e.target.value,
error: undefined,
touched: false,
}));
const handleOnBlur = (e) => {
// NOTE(amine): validate the inputs onBlur and touch the current input
let error = {};
if (validateOnBlur && validate) error = validate(state.value);
setState((prev) => ({ ...prev, touched: validateOnBlur, error }));
};
const handleFormOnSubmit = (e) => {
//NOTE(amine): touch all inputs
setState((prev) => ({ ...prev, touched: true }));
// NOTE(amine): validate inputs
if (validateOnSubmit && validate) {
const error = validate(state.value);
setState((prev) => ({ ...prev, error }));
if (error) return;
}
// NOTE(amine): submit the form
if (!onSubmit) return;
setState((prev) => ({ ...prev, isSubmitting: true }));
onSubmit(state.value)
.then(() => {
setState((prev) => ({ ...prev, isSubmitting: false }));
})
.catch(() => {
setState((prev) => ({ ...prev, isSubmitting: false }));
});
};
// Note(Amine): this prop getter will capture the field state
const getFieldProps = (name) => ({
name: name,
value: state.value,
error: state.error,
touched: state.touched,
onChange: _mergeEventHandlers([onChange, handleFieldChange]),
onBlur: _mergeEventHandlers([onBlur, handleOnBlur]),
onSubmit: handleFormOnSubmit,
});
return { getFieldProps, value: state.value, isSubmitting: state.isSubmitting };
};

View File

@ -1,9 +1,5 @@
export const Logo = (props) => (
<svg
{...props}
fill="none"
viewBox="0 0 236 79"
xmlns="http://www.w3.org/2000/svg">
<svg {...props} fill="none" viewBox="0 0 236 79" xmlns="http://www.w3.org/2000/svg">
<g clipRule="evenodd" fill="currentColor" fillRule="evenodd">
<path d="m217.464 54.8607c-6.06 0-11.06-4.3753-11.135-11.3369h28.241s.214-1.6208.214-3.6082c0-6.7976-1.92-12.0386-5.208-15.5818-3.289-3.5441-7.93-5.3723-13.337-5.3723-5.41 0-10.078 1.9779-13.392 5.6963-3.312 3.7168-5.258 9.1582-5.258 16.0648 0 6.9044 1.846 12.336 5.259 16.0428 3.414 3.7083 8.377 5.6701 14.567 5.6704 5.635.1002 11.16-1.6208 15.742-5.1302l.111-.0854-3.782-6.5418c-1.598 1.2996-3.652 2.3167-5.031 2.9079-1.778.7631-3.915 1.2744-6.991 1.2744zm6.112-25.5668c1.681 1.7391 2.622 4.2517 2.653 7.3749h-19.896c.109-2.7752 1.009-5.2854 2.644-7.1105 1.665-1.8584 3.948-3.0185 7.272-3.0185 3.28 0 5.618.9862 7.327 2.7541z" />
<path d="m196.252 61.3278v-7.7171c-1.658.634-3.09 1.0078-4.02 1.1408-.81.1157-1.407.1123-1.907.1095-1.222 0-2.185-.1755-2.646-.7613-.234-.2976-.406-.7148-.518-1.3028-.113-.588-.164-1.3367-.164-2.2886v-22.8095h9.255v-7.9531h-9.255v-15.40329l-8.724 4.93807v42.30672c0 3.8975.938 6.6181 2.647 8.3634 1.709 1.7459 4.162 2.4879 7.129 2.4879 1.936 0 3.349-.0295 8.203-1.1107z" />
@ -17,11 +13,7 @@ export const Logo = (props) => (
export const Symbol = (props) => {
return (
<svg
{...props}
viewBox="0 0 447 516"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<svg {...props} viewBox="0 0 447 516" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
@ -32,4 +24,42 @@ export const Symbol = (props) => {
);
};
export const DarkSymbol = (props) => {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M10 1.655a4 4 0 014 0l6.392 3.69a4 4 0 012 3.464v7.382a4 4 0 01-2 3.464L14 23.345a4 4 0 01-4 0l-6.392-3.69a4 4 0 01-2-3.464V8.809a4 4 0 012-3.464L10 1.655z"
fill="#4B4A4D"
/>
<g filter="url(#prefix__filter0_d)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16.31 16.594v1.433L19 16.49v-4.342L11.952 8.12v1.432l5.795 3.312v2.907l-1.437.822zm1.437-7.369v1.64l1.253.717V8.508L11.984 4.5 8.187 6.668v4.377l6.373 3.638v2.91l-2.576 1.472-5.731-3.275v-1.639L5 13.433v3.075l6.984 3.992 3.83-2.19v-4.34L9.44 10.327V7.383l2.544-1.454 5.763 3.296zM7.69 8.385V6.95L5 8.492v4.373l7.017 4.01v-1.433l-5.764-3.293v-2.94l1.437-.825z"
fill="#EBEBEB"
/>
</g>
<defs>
<filter
id="prefix__filter0_d"
x={0}
y={2.5}
width={24}
height={26}
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity={0} result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dy={3} />
<feGaussianBlur stdDeviation={2.5} />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter>
</defs>
</svg>
);
};
export default Logo;

View File

@ -131,12 +131,50 @@ export const error = {
SERVER_CREATE_USER_ACCEPT_TERMS: "You must accept the terms of service to create an account",
SERVER_CREATE_USER_USERNAME_TAKEN: "There is already an account with that username",
SERVER_CREATE_USER_INVALID_USERNAME: "Please choose a valid username",
SERVER_CREATE_USER_INVALID_PASSWORD: "Please chooose a valid password",
SERVER_CREATE_USER_INVALID_PASSWORD: "Please choose a valid password",
SERVER_CREATE_USER_INVALID_EMAIL: "Please choose a valid email",
SERVER_CREATE_USER_BUCKET_INIT_FAILURE:
"We're having trouble setting up your storage, please try again later",
SERVER_CREATE_USER_FAILED:
"We're having trouble creating your account right now. Please try again later",
// Twitter
SERVER_CREATE_USER_TWITTER_EXISTS: "There is already an account linked with your twitter",
SERVER_TWITTER_OAUTH_NOT_ALLOWED: "You can only authenticate via twitter while on slate.host",
SERVER_TWITTER_LOGIN_ONLY:
"This login is associated with a Twitter account. Please continue with Twitter instead",
// Email Verifications
SERVER_EMAIL_VERIFICATION_INVALID_PIN: "Please enter a valid pin",
SERVER_EMAIL_VERIFICATION_FAILED:
"We're having trouble with verifying your email, please try again later",
SERVER_CREATE_VERIFICATION_NOT_ALLOWED:
"You can only send a verification pin while on slate.host",
SERVER_CREATE_VERIFICATION_INVALID_EMAIL: "Please choose a valid email",
SERVER_CREATE_VERIFICATION_EMAIL_TAKEN: "There is already an account with this email",
SERVER_CREATE_VERIFICATION_FAILED:
"We're having touble sending a verification pin right now, please try again later",
SERVER_EMAIL_VERIFICATION_NOT_ALLOWED: "You can only verify an email while on slate.host",
// Email Verification Legacy account
SERVER_CREATE_VERIFICATION_INVALID_USERNAME: "Please choose a valid username",
SERVER_CREATE_VERIFICATION_INVALID_PASSWORD: "Please choose a valid password",
SERVER_CREATE_VERIFICATION_WRONG_PASSWORD:
"We were unable to locate that account with those credentials",
// Password Reset
SERVER_RESET_PASSWORD_NOT_ALLOWED: "You can only reset your password while on slate.host",
SERVER_CREATE_VERIFICATION_USER_NOT_FOUND: "We were not able to locate a user with this email",
SERVER_RESET_PASSWORD_FAILED:
"We're having touble resetting your password, please try again late",
SERVER_RESET_PASSWORD_NO_PASSWORD: "Please choose a valid password",
// Migrate User
SERVER_MIGRATE_USER_NOT_ALLOWED: "You can only verify an email while on slate.host",
SERVER_MIGRATE_USER_NO_TOKEN:
"We're having touble sending a verification pin, please try again late",
SERVER_MIGRATE_USER_INVALID_PIN: "Please enter a valid pin",
//Get user
SERVER_GET_USER_NO_USER_PROVIDED:
"We were not able to fetch that user because no user was specified",
@ -159,7 +197,7 @@ export const error = {
SERVER_USER_UPDATE_USERNAME_IS_TAKEN: "There is already an account with that username",
SERVER_USER_UPDATE_DEFAULT_ARCHIVE_CONFIG:
"We're having trouble updating your settings right now",
SERVER_USER_UPDATE_INVALID_PASSWORD: "Please chooose a valid password",
SERVER_USER_UPDATE_INVALID_PASSWORD: "Please choose a valid password",
//Zip files
GET_ZIP_FILES_PATHS_BUCKET_CHECK_FAILED: "We're having trouble locating those files right now",
@ -178,7 +216,7 @@ export const error = {
SERVER_SIGN_IN_NO_USERNAME: "Please provide a username to sign in",
SERVER_SIGN_IN_NO_PASSWORD: "Please provide a password to sign in",
SERVER_SIGN_IN_USER_NOT_FOUND: "We were unable to locate that account with those credentials",
SERVER_SIGN_IN_WRONG_PASSWORD: "We were unable to locate that account with those credentials",
SERVER_SIGN_IN_WRONG_CREDENTIALS: "You have entered an invalid username or password",
//Subscribe
SERVER_SUBSCRIBE_MUST_PROVIDE_SLATE_OR_USER: "No collection or user to follow specified",

View File

@ -34,10 +34,6 @@ export const getByHref = (href, viewer) => {
return { page: { ...activityPage } };
}
if (viewer && pathname === "/_/auth") {
return { page: { ...activityPage } }; //NOTE(martina): authenticated users should be redirected to the home page rather than the
}
let page = navigation.find((each) => pathname.startsWith(each.pathname));
let details;
@ -98,6 +94,7 @@ const authPage = {
pageTitle: "Sign in & Sign up",
ignore: true,
pathname: "/_/auth",
externalAllowed: true,
};
const dataPage = {

View File

@ -432,8 +432,8 @@ export const OldWallet = (props) => (
</svg>
);
export const NavigationArrow = (props) => (
<svg viewBox="0 0 24 24" height={props.height} style={props.style}>
export const RightArrow = (props) => (
<svg viewBox="0 0 24 24" height={props.height} style={props.style} {...props}>
<g
fill="none"
stroke="currentColor"
@ -1857,3 +1857,34 @@ export const RotateCcw = (props) => (
/>
</svg>
);
export const MehCircle = (props) => (
<svg width={16} height={17} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M8 15.167A6.667 6.667 0 108 1.834a6.667 6.667 0 000 13.333zM5.333 10.5h5.334M6 6.5h.007M10 6.5h.007"
stroke="#FF4530"
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export const SmileCircle = (props) => (
<svg width={16} height={17} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M8 15.167A6.667 6.667 0 108 1.834a6.667 6.667 0 000 13.333z"
stroke="#34D159"
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.333 9.834s1 1.333 2.667 1.333c1.667 0 2.667-1.333 2.667-1.333M6 6.5h.007M10 6.5h.007"
stroke="#34D159"
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

View File

@ -52,7 +52,38 @@ export const authenticate = async (state) => {
return response;
};
// NOTE(jim): Signs a user out
export const authenticateViaTwitter = (response) => {
// NOTE(jim): Kills existing session cookie if there is one.
const jwt = cookies.get(Credentials.session.key);
if (jwt) {
cookies.remove(Credentials.session.key, {
path: "/",
maxAge: 3600 * 24 * 7,
sameSite: "strict",
});
}
if (Events.hasError(response)) {
return false;
}
if (response.token) {
// NOTE(jim):
// + One week.
// + Only requests to the same site.
// + Not using sessionStorage so the cookie doesn't leave when the browser dies.
cookies.set(Credentials.session.key, response.token, {
path: "/",
maxAge: 3600 * 24 * 7,
sameSite: "strict",
});
}
return response;
};
// NOTE(jim): Signs a user out and redirects to the sign in screen.
export const signOut = async ({ viewer }) => {
let wsclient = Websockets.getClient();
if (wsclient) {

View File

@ -1,3 +1,5 @@
import BCrypt from "bcryptjs";
//NOTE(martina): this file is for utility functions that do not involve API calls
//For API related utility functions, see common/user-behaviors.js
//And for uploading related utility functions, see common/file-utilities.js
@ -42,6 +44,19 @@ export const endsWithAny = (options, string) =>
}
});
export const encryptPasswordClient = async (text) => {
const salt = "$2a$06$Yl.tEYt9ZxMcem5e6AbeUO";
let hash = text;
const rounds = 5;
for (let i = 1; i <= rounds; i++) {
hash = await BCrypt.hash(text, salt);
console.log(`\nPASSWORD HASH ROUND ${i} COMPLETE\n`);
}
return hash;
};
export const coerceToArray = (input) => {
if (!input) {
return [];

View File

@ -5,7 +5,12 @@ import JSZip from "jszip";
const USERNAME_REGEX = new RegExp("^[a-zA-Z0-9_]{0,}[a-zA-Z]+[0-9]*$");
const MIN_PASSWORD_LENGTH = 8;
const EMAIL_REGEX = /^[\w-]+@[a-zA-Z0-9_]+?\.[a-zA-Z]{2,50}$/;
const EMAIL_REGEX = /^[\w.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9_]+?\.[a-zA-Z]{2,50}$/;
const CONTAINS_DIGIT_REGEX = /\d/;
const CONTAINS_UPPERCASE_REGEX = /[A-Z]/;
const CONTAINS_LOWERCASE_REGEX = /[a-z]/;
const CONTAINS_SYMBOL_REGEX = /[ !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/;
const PIN_REGEX = /^[0-9]{6}$/;
// TODO(jim): Regex should cover some of this.
const REJECT_LIST = [
@ -98,7 +103,30 @@ export const username = (text) => {
return true;
};
export const password = (text) => {
export const email = (text) => {
if (Strings.isEmpty(text)) {
return false;
}
if (text.length > 254 || text.length < 5) {
return false;
}
if (!EMAIL_REGEX.test(text)) {
return false;
}
//NOTE(toast): add this if the sendgrid plan is upgraded
// const sgEmailValidation = validateEmail({ email: text });
// if (sgEmailValidation.verdict !== "Valid") {
// return false;
// }
return true;
};
// NOTE(amine): used to validate old users password (currently only used in signin)
export const legacyPassword = (text) => {
if (Strings.isEmpty(text)) {
return false;
}
@ -110,36 +138,59 @@ export const password = (text) => {
return true;
};
export const isFileTypeAllowed = (type = "") => {
if (type.startsWith("text/")) {
return true;
export const passwordForm = (text) => {
const validations = {
validLength: false,
containsLowerCase: false,
containsUpperCase: false,
containsSymbol: false,
containsNumbers: false,
};
if (Strings.isEmpty(text)) return validations;
if (text.length >= MIN_PASSWORD_LENGTH) validations.validLength = true;
if (CONTAINS_DIGIT_REGEX.test(text)) validations.containsNumbers = true;
if (CONTAINS_LOWERCASE_REGEX.test(text)) validations.containsLowerCase = true;
if (CONTAINS_UPPERCASE_REGEX.test(text)) validations.containsUpperCase = true;
if (CONTAINS_SYMBOL_REGEX.test(text)) validations.containsSymbol = true;
return validations;
};
export const password = (text) => {
if (Strings.isEmpty(text)) {
return false;
}
if (type.startsWith("model/")) {
return true;
if (text.length <= MIN_PASSWORD_LENGTH) {
return false;
}
if (type.startsWith("font/")) {
return true;
let reqCount = 0;
if (CONTAINS_DIGIT_REGEX.test(text)) {
reqCount += 1;
}
if (CONTAINS_LOWERCASE_REGEX.test(text)) {
reqCount += 1;
}
if (CONTAINS_UPPERCASE_REGEX.test(text)) {
reqCount += 1;
}
if (CONTAINS_SYMBOL_REGEX.test(text)) {
reqCount += 1;
}
if (type.startsWith("application/")) {
return true;
return reqCount === 4;
};
export const verificationPin = (pin) => {
if (Strings.isEmpty(pin)) {
return false;
}
if (type.startsWith("audio/")) {
return true;
}
if (type.startsWith("image/")) {
return true;
}
if (type.startsWith("video/")) {
return true;
}
return false;
return PIN_REGEX.test(pin);
};
export const isPreviewableImage = (type = "") => {

View File

@ -74,6 +74,11 @@ const STYLES_TEXT = css`
const STYLES_MESSAGE_BOX = css`
display: flex;
align-items: center;
//Note(amine): remove bottom padding from svg
svg {
display: block;
}
`;
export class Alert extends React.Component {

View File

@ -22,7 +22,7 @@ import SceneFilesFolder from "~/scenes/SceneFilesFolder";
import SceneSettings from "~/scenes/SceneSettings";
import SceneSlates from "~/scenes/SceneSlates";
import SceneSettingsDeveloper from "~/scenes/SceneSettingsDeveloper";
import SceneSignIn from "~/scenes/SceneSignIn";
import SceneAuth from "~/scenes/SceneAuth";
import SceneSlate from "~/scenes/SceneSlate";
import SceneActivity from "~/scenes/SceneActivity";
import SceneDirectory from "~/scenes/SceneDirectory";
@ -77,7 +77,7 @@ const SIDEBARS = {
const SCENES = {
NAV_ERROR: <SceneError />,
NAV_SIGN_IN: <SceneSignIn />,
NAV_SIGN_IN: <SceneAuth />,
NAV_ACTIVITY: <SceneActivity />,
NAV_DIRECTORY: <SceneDirectory />,
NAV_PROFILE: <SceneProfile />,
@ -461,27 +461,30 @@ export default class ApplicationPage extends React.Component {
e.preventDefault();
};
_handleCreateUser = async (state) => {
let response = await Actions.createUser(state);
// _handleCreateUser = async (state) => {
// let response = await Actions.createUser(state);
if (!response || response.error) {
// if (Events.hasError(response)) {
// return;
// }
// return this._handleAuthenticate(state, true);
// };
_withAuthenticationBehavior = (authenticate) => async (state, newAccount) => {
let response = await authenticate(state);
if (Events.hasError(response)) {
return response;
}
return this._handleAuthenticate(state, true);
};
_handleAuthenticate = async (state, newAccount) => {
let response = await UserBehaviors.authenticate(state);
if (!response || response.error) {
if (response.shouldMigrate) {
return response;
}
let viewer = await UserBehaviors.hydrate();
if (!viewer || viewer.error) {
if (Events.hasError(viewer)) {
return viewer;
}
this.setState({ viewer });
this.setState({ viewer }, () => console.log(this.state.viewer));
await this._handleSetupWebsocket();
let unseenAnnouncements = [];
@ -516,6 +519,8 @@ export default class ApplicationPage extends React.Component {
// if (!redirected) {
// this._handleAction({ type: "NAVIGATE", value: "NAV_DATA" });
// }
window.location.replace("/_/activity");
return response;
};
@ -714,8 +719,8 @@ export default class ApplicationPage extends React.Component {
viewer: this.state.viewer,
selected: this.state.selected,
onSelectedChange: this._handleSelectedChange,
onAuthenticate: this._handleAuthenticate,
onCreateUser: this._handleCreateUser,
onAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticate),
onTwitterAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticateViaTwitter),
onAction: this._handleAction,
onUpload: this._handleUploadFiles,
isMobile: this.state.isMobile,

View File

@ -0,0 +1,227 @@
import * as React from "react";
import * as System from "~/components/system";
import * as SVG from "~/common/svg";
import * as Actions from "~/common/actions";
import * as Validations from "~/common/validations";
import * as Events from "~/common/custom-events";
import * as Strings from "~/common/strings";
import * as Styles from "~/common/styles";
import { css } from "@emotion/react";
import { motion, AnimateSharedLayout } from "framer-motion";
import { useForm, useField } from "~/common/hooks";
import Field from "~/components/core/Field";
import { Toggle, SignUpPopover } from "~/components/core/Auth/components";
const STYLES_INITIAL_CONTAINER = css`
display: flex;
flex-direction: column;
height: 100%;
`;
const STYLES_LINK_ITEM = (theme) => css`
display: block;
text-decoration: none;
font-weight: 400;
font-size: 14px;
font-family: ${theme.font.medium};
user-select: none;
cursor: pointer;
margin-top: 2px;
color: ${theme.system.black};
transition: 200ms ease all;
word-wrap: break-word;
:visited {
color: ${theme.system.black};
}
:hover {
color: ${theme.system.brand};
}
`;
// NOTE(amine): used to remove content jumping
// when switching between signin/signup in mobile
const STYLES_SPACER = (theme) => css`
height: 20px;
@media (max-width: ${theme.sizes.mobile}px) {
height: 10vh;
}
`;
const useToggler = ({ params }) => {
const TOGGLE_OPTIONS = [
{ value: "signin", label: "Sign in" },
{ value: "signup", label: "Sign up" },
];
const [state, setState] = React.useState(params?.tab || "signin");
const handleToggleChange = (value) => setState(value);
return { toggleValue: state, handleToggleChange, TOGGLE_OPTIONS };
};
export default function Initial({
isSigninViaTwitter,
goToSigninScene,
onTwitterSignin,
goToSignupScene,
createVerification,
initialEmail,
page,
}) {
const { TOGGLE_OPTIONS, toggleValue, handleToggleChange } = useToggler(page);
// NOTE(amine): Signup view form
const { getFieldProps, getFormProps, isSubmitting: isCheckingEmail } = useForm({
validateOnBlur: false,
initialValues: { email: initialEmail || "" },
validate: ({ email }, errors) => {
if (Strings.isEmpty(email)) {
errors.email = "Please provide an email";
} else if (!Validations.email(email)) {
errors.email = "Invalid email address";
}
return errors;
},
onSubmit: async ({ email }) => {
const response = await Actions.checkEmail({ email });
if (response?.data?.twitter) {
Events.dispatchMessage({
message: "This email is associated with a Twitter account, please log in with Twitter",
});
return;
}
if (response?.data?.email) {
goToSigninScene({
emailOrUsername: email,
message: `There is already an account associated with ${email}`,
});
return;
}
const verificationResponse = await createVerification({ email });
if (!verificationResponse) return;
goToSignupScene({ email });
},
});
// NOTE(amine): Signin view form
const { getFieldProps: getSigninFieldProps } = useField({
validateOnBlur: false,
onSubmit: async (emailOrUsername) => goToSigninScene({ emailOrUsername }),
validate: (emailOrUsername) => {
if (Strings.isEmpty(emailOrUsername)) return "Please enter a username or email";
if (!Validations.username(emailOrUsername) && !Validations.email(emailOrUsername))
return "Invalid email/username";
},
initialValue: "",
});
return (
<SignUpPopover
title={<>Discover, experience, share files on Slate</>}
style={{ paddingBottom: 24 }}
>
<div css={STYLES_INITIAL_CONTAINER}>
<div css={STYLES_SPACER} />
<div
css={{
display: "flex",
flexDirection: "column",
alignItem: "center",
justifyContent: "center",
}}
>
<System.ButtonPrimary
full
style={{ backgroundColor: "rgb(29,161,242)" }}
onClick={onTwitterSignin}
loading={isSigninViaTwitter}
>
Continue with Twitter
</System.ButtonPrimary>
<System.Divider
color="#AEAEB2"
width="45px"
height="0.5px"
style={{ margin: "0px auto", marginTop: "20px" }}
/>
<Toggle
toggleValue={toggleValue}
options={TOGGLE_OPTIONS}
style={{ margin: "20px auto 0px" }}
onChange={handleToggleChange}
/>
</div>
{toggleValue === "signin" ? (
<>
<Field
autoFocus
label="Email address or username"
placeholder="email/username"
icon={SVG.RightArrow}
name="email"
type="text"
full
{...getSigninFieldProps()}
// NOTE(amine): the input component internally is using 16px margin top
containerStyle={{ marginTop: "4px" }}
/>
<div style={{ marginTop: "auto" }}>
<a css={STYLES_LINK_ITEM} href="/terms" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Terms of service
</div>
</a>
<a css={STYLES_LINK_ITEM} style={{ marginTop: 4 }} href="/guidelines" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Community guidelines
</div>
</a>
</div>
</>
) : (
<AnimateSharedLayout>
<form {...getFormProps()}>
<Field
autoFocus
label="Sign up with email"
placeholder="Email"
full
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
// NOTE(amine): the input component internally is using 16px margin top
containerStyle={{ marginTop: "4px" }}
{...getFieldProps("email")}
/>
<motion.div layout>
<System.ButtonPrimary
full
type="submit"
style={{ marginTop: "16px" }}
loading={isCheckingEmail}
>
Send verification link
</System.ButtonPrimary>
</motion.div>
</form>
<div style={{ marginTop: "auto" }}>
<a css={STYLES_LINK_ITEM} href="/terms" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Terms of service
</div>
</a>
<a css={STYLES_LINK_ITEM} style={{ marginTop: 4 }} href="/guidelines" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Community guidelines
</div>
</a>
</div>
</AnimateSharedLayout>
)}
</div>
</SignUpPopover>
);
}

View File

@ -0,0 +1,183 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import * as Actions from "~/common/actions";
import * as Utilities from "~/common/utilities";
import * as SVG from "~/common/svg";
import Field from "~/components/core/Field";
import { AnimateSharedLayout, motion } from "framer-motion";
import { P } from "~/components/system";
import { useForm } from "~/common/hooks";
import { css } from "@emotion/react";
import { SignUpPopover, Verification } from "~/components/core/Auth/components";
const STYLES_BACK_BUTTON = css`
background: none;
border-style: none;
cursor: pointer;
margin-top: auto;
margin-right: auto;
span {
display: flex;
align-items: center;
justify-content: center;
& > * + * {
margin-left: 8px;
}
}
`;
const handleValidation = ({ email }, errors) => {
if (!Validations.email(email)) errors.email = "Invalid email";
return errors;
};
const handleNewPasswordValidation = ({ password }, errors) => {
if (!Validations.password(password)) errors.password = "Invalid Password";
return errors;
};
const usePasswordReset = () => {
// NOTE(amine): can be either initial || verification || new_password
const [scene, setScene] = React.useState("initial");
const handlers = React.useMemo(
() => ({
goToVerificationScene: () => setScene("verification"),
goToNewPasswordScene: () => setScene("new_password"),
}),
[]
);
return { ...handlers, scene };
};
export default function ResetPassword({
goBack,
createVerification,
verifyEmail,
resetPassword,
resendEmailVerification,
}) {
const [passwordValidations, setPasswordValidations] = React.useState(
Validations.passwordForm("")
);
const { scene, goToNewPasswordScene, goToVerificationScene } = usePasswordReset();
// NOTE(amine): Asking for email scene form
const { getFieldProps, getFormProps, isSubmitting, values } = useForm({
validateOnBlur: false,
initialValues: { email: "" },
validate: handleValidation,
onSubmit: async ({ email }) => {
const response = await createVerification({ email });
if (!response) return;
goToVerificationScene();
},
});
// NOTE(amine): New password scene form
const {
getFieldProps: getNewPasswordFieldProps,
getFormProps: getNewPasswordFormProps,
isSubmitting: isNewPasswordFormSubmitting,
} = useForm({
validateOnBlur: false,
initialValues: { password: "" },
validate: handleNewPasswordValidation,
onSubmit: async ({ password }) => {
await resetPassword({ password });
},
});
if (scene === "verification") {
const handleVerification = async ({ pin }) => {
const response = await verifyEmail({ pin });
if (!response) return;
goToNewPasswordScene();
};
return (
<Verification
title={`Password reset code sent to ${values.email}`}
onVerify={handleVerification}
onResend={resendEmailVerification}
/>
);
}
if (scene === "new_password") {
return (
<SignUpPopover title={<>Enter new password</>}>
<form {...getNewPasswordFormProps()} style={{ marginTop: 72 }}>
<Field
autoFocus
containerStyle={{ marginTop: 16 }}
placeholder="new password"
type="password"
full
validations={passwordValidations}
{...getNewPasswordFieldProps("password", {
onChange: (e) => {
const validations = Validations.passwordForm(e.target.value);
setPasswordValidations(validations);
},
})}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>
<System.ButtonPrimary
full
type="submit"
loading={isNewPasswordFormSubmitting}
style={{ marginTop: 16 }}
>
Log in with new password
</System.ButtonPrimary>
</motion.div>
</AnimateSharedLayout>
</form>
</SignUpPopover>
);
}
return (
<SignUpPopover
title={
<>
Enter your email <br /> to reset password
</>
}
>
<form {...getFormProps()} style={{ marginTop: 72 }}>
<Field
autoFocus
containerStyle={{ marginTop: 16 }}
placeholder="Email"
type="text"
full
{...getFieldProps("email")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>
<System.ButtonPrimary
full
type="submit"
loading={isSubmitting}
style={{ marginTop: 16 }}
>
Send password reset code
</System.ButtonPrimary>
</motion.div>
</AnimateSharedLayout>
</form>
<button css={STYLES_BACK_BUTTON} type="button" onClick={goBack}>
<span>
<SVG.RightArrow height="16px" style={{ transform: "rotate(180deg)" }} />
<P>Back</P>
</span>
</button>
</SignUpPopover>
);
}

View File

@ -0,0 +1,229 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import * as Styles from "~/common/styles";
import Field from "~/components/core/Field";
import { P } from "~/components/system";
import { AnimateSharedLayout, motion } from "framer-motion";
import { useForm } from "~/common/hooks";
import { css } from "@emotion/react";
import { SignUpPopover, Verification } from "~/components/core/Auth/components";
const STYLES_BACK_BUTTON = css`
color: ${Constants.system.textGrayDark};
background: none;
border-style: none;
cursor: pointer;
margin-top: auto;
margin-right: auto;
span {
display: flex;
align-items: center;
justify-content: center;
& > * + * {
margin-left: 4px;
}
}
`;
const STYLES_FORGOT_PASSWORD_BUTTON = (theme) => css`
display: block;
background: none;
border: none;
margin: 16px auto 0px;
padding: 8px 0;
cursor: pointer;
& > * {
font-size: ${theme.typescale.lvl0};
color: ${theme.system.blue};
}
`;
const STYLES_MESSAGE = (theme) => css`
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-top: 16px;
border: 1px solid ${theme.system.white};
background-color: white;
@supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) {
background-color: ${theme.system.bgBlurWhiteTRN};
backdrop-filter: blur(75px);
}
padding: 8px 12px;
border-radius: 8px;
& > * + * {
margin-left: 16px;
}
`;
const STYLES_MESSAGE_PARAGRAPH = (theme) => css`
font-size: ${theme.typescale.lvlN1};
color: ${theme.system.blue};
`;
const STYLES_MESSAGE_BUTTON = (theme) => css`
background: none;
border: none;
padding: 0;
margin: 0;
svg {
color: ${theme.system.textGrayDark};
height: 16px;
width: 16px;
}
`;
const handleValidation = ({ password }, errors) => {
if (!Validations.legacyPassword(password)) errors.password = "Incorrect password";
return errors;
};
const handleNewPasswordValidation = ({ email }, errors) => {
if (Strings.isEmpty(email)) errors.email = "Please enter an email";
if (!Validations.email(email)) errors.email = "Invalid email";
return errors;
};
const useSignin = () => {
// NOTE(amine): can be either initial || email_request || verification
const [scene, setScene] = React.useState("initial");
const handlers = React.useMemo(
() => ({
goToEmailRequestScene: () => setScene("email_request"),
goToVerificationScene: () => setScene("verification"),
}),
[]
);
return { ...handlers, scene };
};
export default function Signin({
emailOrUsername = "",
message,
signin,
createVerification,
migrateAccount,
goToResetPassword,
resendEmailVerification,
goBack,
clearMessages,
}) {
const { scene, goToEmailRequestScene, goToVerificationScene } = useSignin();
const [showPassword, toggleShowPassword] = React.useState(false);
// NOTE(amine): Signin Form
const { getFieldProps, getFormProps, isSubmitting } = useForm({
validateOnBlur: false,
initialValues: { username: emailOrUsername, password: "" },
validate: handleValidation,
onSubmit: async ({ username, password }) => {
const response = await signin({ username, password });
if (response?.shouldMigrate) goToEmailRequestScene();
},
});
// NOTE(amine): verify email scene form
const {
getFieldProps: getEmailFieldProps,
getFormProps: getEmailFormProps,
isSubmitting: isEmailFormSubmitting,
} = useForm({
validateOnBlur: false,
initialValues: { email: "" },
validate: handleNewPasswordValidation,
onSubmit: async ({ email }) => {
const response = await createVerification({ email });
if (!response) return;
goToVerificationScene();
},
});
if (scene === "verification") {
const handleVerification = async ({ pin }) => await migrateAccount({ pin });
return <Verification onVerify={handleVerification} onResend={resendEmailVerification} />;
}
if (scene === "email_request") {
return (
<SignUpPopover title={`Please add an email address for ${emailOrUsername}`}>
<form {...getEmailFormProps()} style={{ marginTop: 72 }}>
<Field
autoFocus
containerStyle={{ marginTop: 16 }}
placeholder="Email"
type="text"
full
{...getEmailFieldProps("email")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>
<System.ButtonPrimary
full
type="submit"
loading={isEmailFormSubmitting}
style={{ marginTop: 16 }}
>
Send verification code
</System.ButtonPrimary>
</motion.div>
</AnimateSharedLayout>
</form>
</SignUpPopover>
);
}
return (
<SignUpPopover title={`Enter Password for ${emailOrUsername}`}>
<form {...getFormProps()} style={{ marginTop: message ? 24 : 41 }}>
{message && (
<div css={STYLES_MESSAGE}>
<P css={STYLES_MESSAGE_PARAGRAPH}>{message}</P>
<button css={STYLES_MESSAGE_BUTTON} onClick={clearMessages}>
<SVG.Dismiss />
</button>
</div>
)}
<Field
autoFocus
containerStyle={{ marginTop: message ? 24 : 16 }}
placeholder="Password"
type={showPassword ? "text" : "password"}
full
onClickIcon={() => toggleShowPassword(!showPassword)}
icon={showPassword ? SVG.EyeOff : SVG.Eye}
{...getFieldProps("password")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>
<System.ButtonPrimary
full
type="submit"
loading={isSubmitting}
style={{ marginTop: 16 }}
>
Sign in
</System.ButtonPrimary>
<button type="button" onClick={goToResetPassword} css={STYLES_FORGOT_PASSWORD_BUTTON}>
<P css={Styles.HEADING_05}> Forgot Password?</P>
</button>
</motion.div>
</AnimateSharedLayout>
</form>
<button css={STYLES_BACK_BUTTON} type="button" onClick={goBack}>
<span>
<SVG.RightArrow height="16px" style={{ transform: "rotate(180deg)" }} />
<P css={Styles.HEADING_05}>Back</P>
</span>
</button>
</SignUpPopover>
);
}

View File

@ -0,0 +1,100 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import Field from "~/components/core/Field";
import { AnimateSharedLayout, motion } from "framer-motion";
import { useForm } from "~/common/hooks";
import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Auth/components";
const useSignup = () => {
const [scene, setScene] = React.useState("verification");
const handlers = React.useMemo(
() => ({
goToAccountCreationScene: () => setScene("accountCreation"),
}),
[]
);
return { ...handlers, scene };
};
const handleValidation = ({ username, password, acceptTerms }, errors) => {
if (!Validations.username(username)) errors.username = "Invalid username";
// Note(amine): username should not be an email
if (Validations.email(username)) errors.username = "Username shouldn't be an email";
if (!Validations.password(password)) errors.password = "Incorrect password";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
return errors;
};
export default function Signup({ verifyEmail, createUser, resendEmailVerification }) {
const [passwordValidations, setPasswordValidations] = React.useState(
Validations.passwordForm("")
);
const { goToAccountCreationScene, scene } = useSignup();
const { getFieldProps, getFormProps, isSubmitting } = useForm({
initialValues: { username: "", password: "", acceptTerms: false },
validate: handleValidation,
onSubmit: async ({ username, password }) => await createUser({ username, password }),
});
if (scene === "verification") {
const handleVerification = async ({ pin }) => {
const response = await verifyEmail({ pin });
if (response) {
goToAccountCreationScene();
}
};
return <Verification onVerify={handleVerification} onResend={resendEmailVerification} />;
}
return (
<SignUpPopover title="Create an account">
<AnimateSharedLayout>
<form {...getFormProps()}>
<Field
autoFocus
containerStyle={{ marginTop: 46 }}
placeholder="username"
type="text"
full
{...getFieldProps("username")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<motion.div layout>
<Field
containerStyle={{ marginTop: 16 }}
placeholder="password"
type="password"
full
validations={passwordValidations}
{...getFieldProps("password", {
onChange: (e) => {
const validations = Validations.passwordForm(e.target.value);
setPasswordValidations(validations);
},
})}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AuthCheckBox style={{ marginTop: "16px" }} {...getFieldProps("acceptTerms")} />
<System.ButtonPrimary
full
style={{ marginTop: "36px" }}
loading={isSubmitting}
type="submit"
>
Create account
</System.ButtonPrimary>
</motion.div>
</form>
</AnimateSharedLayout>
</SignUpPopover>
);
}

View File

@ -0,0 +1,122 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import Field from "~/components/core/Field";
import { AnimateSharedLayout, motion } from "framer-motion";
import { css } from "@emotion/react";
import { useForm } from "~/common/hooks";
import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Auth/components";
const STYLES_SMALL = (theme) => css`
font-size: ${theme.typescale.lvlN1};
text-align: center;
color: ${theme.system.textGrayDark};
max-width: 228px;
margin: 0 auto;
`;
const useTwitterSignup = () => {
const [scene, setScene] = React.useState("accountCreation");
const handlers = React.useMemo(() => ({ goToVerification: () => setScene("verification") }), []);
return { ...handlers, scene };
};
const handleValidation = ({ username, email, acceptTerms }, errors) => {
if (!Validations.username(username)) errors.username = "Invalid username";
// Note(amine): username should not be an email
if (Validations.email(username)) errors.username = "Username shouldn't be an email";
if (!Validations.email(email)) errors.email = "Invalid email";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
return errors;
};
const MotionLayout = ({ children, ...props }) => (
<motion.div layout {...props}>
{children}
</motion.div>
);
export default function TwitterSignup({
initialEmail,
onSignup,
createVerification,
onSignupWithVerification,
}) {
const { scene, goToVerification } = useTwitterSignup();
const {
getFieldProps,
getFormProps,
values: { email, username },
isSubmitting,
} = useForm({
initialValues: { username: "", email: initialEmail, acceptTerms: false },
validate: handleValidation,
onSubmit: async ({ username, email }) => {
if (email !== initialEmail) {
const response = await createVerification({ email });
if (!response) return;
goToVerification();
return;
}
await onSignup({ email, username });
},
});
if (scene === "verification") {
const handleVerification = async ({ pin }) => {
await onSignupWithVerification({ username, pin });
};
return <Verification onVerify={handleVerification} />;
}
return (
<SignUpPopover title="Create an account">
<form {...getFormProps()}>
<Field
autoFocus
containerStyle={{ marginTop: 41 }}
placeholder="Username"
type="username"
{...getFieldProps("username")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<Field
containerStyle={{ marginTop: 16 }}
containerAs={MotionLayout}
errorAs={MotionLayout}
placeholder="Email"
type="email"
{...getFieldProps("email")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<motion.div layout>
<AuthCheckBox style={{ marginTop: 16 }} {...getFieldProps("acceptTerms")} />
<System.ButtonPrimary
full
style={{ marginTop: 36 }}
loading={isSubmitting}
type="submit"
>
Create account
</System.ButtonPrimary>
</motion.div>
{(!initialEmail || initialEmail !== email) && (
<motion.div layout>
<System.P css={STYLES_SMALL} style={{ marginTop: 16 }}>
You will receive a code to verify your email at this address
</System.P>
</motion.div>
)}
</AnimateSharedLayout>
</form>
</SignUpPopover>
);
}

View File

@ -0,0 +1,64 @@
import * as React from "react";
import * as System from "~/components/system";
import { css } from "@emotion/react";
const STYLES_CHECKBOX_LABEL = (theme) => css`
padding-left: 8px;
a,
a:link,
a:visited {
color: ${theme.system.blue};
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
`;
const STYLES_CHECKBOX_INPUT = css`
background-color: rgba(242, 242, 247, 0.5);
height: 16px;
width: 16px;
`;
const STYLES_CHECKBOX_ERROR = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.red};
height: 16px;
width: 16px;
`;
const STYLES_CHECKBOX_SUCCESS = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.green};
height: 16px;
width: 16px;
`;
const STYLES_CHECKBOX_WRAPPER = css`
align-items: center;
`;
export default function AuthCheckBox({ touched, error, ...props }) {
const showError = touched && error;
const showSuccess = touched && !error;
const STYLES_CHECKBOX = React.useMemo(() => {
if (showError) return STYLES_CHECKBOX_ERROR;
return STYLES_CHECKBOX_INPUT;
}, [touched, error]);
return (
<System.CheckBox
containerStyles={STYLES_CHECKBOX_WRAPPER}
labelStyles={STYLES_CHECKBOX_LABEL}
inputStyles={STYLES_CHECKBOX}
{...props}
>
I agree to the Slate{" "}
<a href="/terms" target="_blank" style={{ textDecoration: "none" }}>
terms of service
</a>
</System.CheckBox>
);
}

View File

@ -0,0 +1,74 @@
import * as React from "react";
import * as System from "~/components/system";
import { DarkSymbol } from "~/common/logo";
import { css } from "@emotion/react";
const STYLES_POPOVER = (theme) => css`
display: flex;
flex-direction: column;
box-sizing: border-box;
max-width: 432px;
height: 544px;
width: 95vw;
border-radius: 8px;
padding: 36px 32px;
@media (max-width: ${theme.sizes.mobile}px) {
flex-grow: 1;
margin-bottom: auto;
max-width: 100%;
}
background-color: white;
@supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) {
background: radial-gradient(
80.79% 80.79% at 50% 50%,
rgba(242, 242, 247, 0.5) 0%,
rgba(242, 242, 247, 0) 100%
);
backdrop-filter: blur(75px);
}
@keyframes authentication-popover-fade-in {
from {
transform: translateY(-8px);
opacity: 0;
}
to {
transform: translateY(0px);
opacity: 1;
}
}
animation: authentication-popover-fade-in 400ms ease;
`;
const STYLES_POPOVER_BODY = (theme) => css`
display: flex;
height: 100%;
flex-direction: column;
justify-content: flex-start;
@media (max-width: ${theme.sizes.mobile}px) {
justify-content: center;
}
`;
export default function SignUpPopover({ children, title, logoStyle, titleStyle, props }) {
return (
<div css={STYLES_POPOVER} {...props}>
<div>
<div style={{ textAlign: "center" }}>
<DarkSymbol height="40" width="40" style={{ marginBottom: "8px", ...logoStyle }} />
</div>
<System.H3
style={{ textAlign: "center", lineHeight: "30px", padding: "0 24px", ...titleStyle }}
>
{title}
</System.H3>
</div>
<div css={STYLES_POPOVER_BODY}>{children}</div>
</div>
);
}

View File

@ -0,0 +1,75 @@
import * as React from "react";
import { motion, AnimateSharedLayout } from "framer-motion";
import { css } from "@emotion/react";
const STYLES_WRAPPER = (theme) => css`
width: fit-content;
display: flex;
background-color: ${theme.system.bgGrayLight};
border-radius: 8px;
`;
const STYLES_BUTTON = (theme) => css`
border-style: none;
padding: 8px 16px;
border-radius: 8px;
font-family: ${theme.font.medium};
font-size: ${theme.typescale.lvl0};
color: ${theme.system.textGray};
background-color: transparent;
outline-style: none;
cursor: pointer;
transition: 0.2 color;
`;
const STYLES_ACTIVE = (theme) => css`
background-color: ${theme.system.white};
color: ${theme.system.textBlack};
`;
const STYLES_BUTTON_ACTIVE = (theme) => css`
color: ${theme.system.textBlack};
`;
export default function Toggle({ options = [], onChange, toggleValue = "signin", ...props }) {
const initialValue = toggleValue === "signup" ? 1 : 0;
const [currentOption, setOption] = React.useState(options[initialValue]);
const handleChange = (nextValue) => {
if (onChange) onChange(nextValue.value);
setOption(nextValue);
};
return (
<AnimateSharedLayout>
<div css={STYLES_WRAPPER} {...props}>
{options.map((option) => (
<div key={option.label} style={{ position: "relative" }}>
<button
type="button"
style={{ position: "relative", zIndex: 3 }}
onClick={() => handleChange(option)}
css={[STYLES_BUTTON, option.value === currentOption.value && STYLES_BUTTON_ACTIVE]}
>
{option.label}
</button>
{option.value === currentOption.value && (
<motion.div
layoutId="button"
style={{
position: "absolute",
top: 0,
zIndex: 1,
left: 0,
width: "100%",
borderRadius: "8px",
height: "100%",
}}
css={STYLES_ACTIVE}
/>
)}
</div>
))}
</div>
</AnimateSharedLayout>
);
}

View File

@ -0,0 +1,126 @@
import * as React from "react";
import * as System from "~/components/system";
import * as SVG from "~/common/svg";
import * as Validations from "~/common/validations";
import Field from "~/components/core/Field";
import { AnimateSharedLayout, motion } from "framer-motion";
import { LoaderSpinner } from "~/components/system/components/Loaders";
import { css } from "@emotion/react";
import { useField } from "~/common/hooks";
import { SignUpPopover } from "~/components/core/Auth/components";
const STYLES_HELPER = (theme) => css`
text-align: center;
margin-top: 8px;
font-size: ${theme.typescale.lvl0};
color: ${theme.system.textGrayDark};
`;
const STYLES_RESEND_BUTTON = (theme) => css`
padding: 0;
margin: 0;
font-size: ${theme.typescale.lvl0};
color: ${theme.system.blue};
border: none;
background: transparent;
cursor: pointer;
`;
const getResendText = ({ status, timeLeft }) => {
if (status === "ready") return "Resend code.";
if (status === "sending") return "Sending code...";
if (status === "sent") return "Code sent.";
return `Resend code in ${timeLeft}s`;
};
const ResendButton = ({ onResend }) => {
// NOTE(amine): we have 4 status: ready, sending, sent, timeout
const [status, setStatus] = React.useState("ready");
const [timer, setTimer] = React.useState(35);
const handleResend = async () => {
if (status === "ready") {
setStatus("sending");
await onResend();
setStatus("sent");
return;
}
if (status === "sent") {
setStatus("timeout");
}
};
// NOTE(amine): when the timer hits 0,
React.useEffect(() => {
if (timer === 0) setStatus("ready");
}, [timer]);
React.useEffect(() => {
let interval;
// NOTE(amine): start a timer when the email is sent
if (status === "sent") {
// NOTE(amine): reset timer to 35s
setTimer(35);
interval = setInterval(() => {
setTimer((prev) => prev - 1);
//NOTE(amine): clear interval when the timer is over.
if (timer === 0) clearInterval(interval);
}, 1000);
}
}, [status]);
return (
<System.P css={STYLES_RESEND_BUTTON} style={{ display: "inline" }} onClick={handleResend}>
{getResendText({ status, timeLeft: timer })}
</System.P>
);
};
const DEFAULT_TITLE = (
<>
Verification code sent,
<br />
please check your inbox.
</>
);
export default function Verification({ onVerify, title = DEFAULT_TITLE, onResend }) {
const { getFieldProps, isSubmitting } = useField({
validateOnBlur: false,
initialValue: "",
onSubmit: async (pin) => onVerify({ pin }),
validate: (pin) => {
if (!Validations.verificationPin(pin)) return "Invalid pin";
},
});
return (
<SignUpPopover logoStyle={{ width: 56, height: 56 }} title={title}>
<Field
autoFocus
label="Enter the 6 digit code sent to your email"
full
icon={
isSubmitting
? (props) => <LoaderSpinner style={{ height: 16, width: 16, marginLeft: 16 }} />
: SVG.RightArrow
}
containerStyle={{ marginTop: "28px" }}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
name="pin"
type="pin"
{...getFieldProps()}
/>
<AnimateSharedLayout>
<motion.div layout>
<System.P css={STYLES_HELPER}>
Didnt receive an email? <ResendButton onResend={onResend} />
</System.P>
</motion.div>
</AnimateSharedLayout>
</SignUpPopover>
);
}

View File

@ -0,0 +1,4 @@
export { default as Toggle } from "~/components/core/Auth/components/Toggle";
export { default as SignUpPopover } from "~/components/core/Auth/components/SignUpPopover";
export { default as Verification } from "~/components/core/Auth/components/Verification";
export { default as AuthCheckBox } from "~/components/core/Auth/components/AuthCheckBox";

View File

@ -0,0 +1,5 @@
export { default as Signin } from "~/components/core/Auth/Signin";
export { default as Signup } from "~/components/core/Auth/Signup";
export { default as Initial } from "~/components/core/Auth/Initial";
export { default as TwitterSignup } from "~/components/core/Auth/TwitterSignup";
export { default as ResetPassword } from "~/components/core/Auth/ResetPassword";

140
components/core/Field.js Normal file
View File

@ -0,0 +1,140 @@
import * as React from "react";
import * as SVG from "~/common/svg";
import { P } from "~/components/system/components/Typography";
import { Input } from "~/components/system";
import { css } from "@emotion/react";
const STYLES_SMALL_TEXT = (theme) => css`
font-size: ${theme.typescale.lvlN1};
line-height: 15.6px;
`;
const STYLES_PASSWORD_VALIDATIONS = (theme) => css`
margin-top: 16px;
border: 1px solid ${theme.system.white};
background-color: white;
@supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) {
background-color: ${theme.system.bgBlurWhiteTRN};
backdrop-filter: blur(75px);
}
padding: 8px 12px;
border-radius: 8px;
`;
const STYLES_PASSWORD_VALIDATION = (theme) =>
css`
display: flex;
align-items: center;
& > * + * {
margin-left: 8px;
}
`;
const STYLES_CIRCLE = (theme) => css`
height: 8px;
width: 8px;
border-radius: 50%;
border: 1.25px solid ${theme.system.grayLight2};
`;
const STYLES_CIRCLE_SUCCESS = (theme) => css`
border: 1.25px solid ${theme.system.green};
`;
const STYLES_INPUT = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border-radius: 8px;
&::placeholder {
color: ${theme.system.textGrayDark};
}
`;
const STYLES_INPUT_ERROR = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.red};
&::placeholder {
color: ${theme.system.textGrayDark};
}
`;
const STYLES_INPUT_SUCCESS = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.green};
&::placeholder {
color: ${theme.system.textGrayDark};
}
`;
const PasswordValidations = ({ validations }) => {
return (
<div css={STYLES_PASSWORD_VALIDATIONS}>
<P css={STYLES_SMALL_TEXT}>Passwords should</P>
<div css={STYLES_PASSWORD_VALIDATION}>
<div css={[STYLES_CIRCLE, validations.validLength && STYLES_CIRCLE_SUCCESS]} />
<P css={STYLES_SMALL_TEXT}>Be at least 8 characters long</P>
</div>
<div css={STYLES_PASSWORD_VALIDATION}>
<div
css={[
STYLES_CIRCLE,
validations.containsLowerCase && validations.containsUpperCase && STYLES_CIRCLE_SUCCESS,
]}
/>
<P css={STYLES_SMALL_TEXT}>Contain both uppercase and lowercase letters</P>
</div>
<div css={STYLES_PASSWORD_VALIDATION}>
<div css={[STYLES_CIRCLE, validations.containsNumbers && STYLES_CIRCLE_SUCCESS]} />
<P css={STYLES_SMALL_TEXT}>Contain at least 1 number</P>
</div>
<div css={STYLES_PASSWORD_VALIDATION}>
<div css={[STYLES_CIRCLE, validations.containsSymbol && STYLES_CIRCLE_SUCCESS]} />
<P css={STYLES_SMALL_TEXT}>Contain at least 1 symbol</P>
</div>
</div>
);
};
export default function Field({
touched,
error,
icon,
validations,
errorAs,
containerAs,
...props
}) {
const showError = touched && error;
const showSuccess = touched && !error;
// const Icon = React.useMemo(() => {
// if (icon) return showError ? SVG.MehCircle : icon;
// if (showError) return SVG.MehCircle;
// if (showSuccess) return SVG.SmileCircle;
// }, [touched, error]);
const STYLES = React.useMemo(() => {
if (showError) return STYLES_INPUT_ERROR;
if (showSuccess) return STYLES_INPUT_SUCCESS;
return STYLES_INPUT;
}, [touched, error]);
const ContainerComponent = containerAs || "div";
const ErrorWrapper = errorAs || "div";
return (
<div>
<ContainerComponent>
<Input icon={icon} inputCss={STYLES} {...props} />
</ContainerComponent>
{props.type === "password" && validations ? (
<PasswordValidations validations={validations} />
) : (
<ErrorWrapper>
<P css={STYLES_SMALL_TEXT} style={{ marginTop: "8px" }}>
{showError && error}
</P>
</ErrorWrapper>
)}
</div>
);
}

View File

@ -6,7 +6,7 @@ import { P } from "~/components/system/components/Typography";
import { Slider } from "~/components/system/components/Slider";
import { css } from "@emotion/react";
import Select from "./Select";
import Select from "~/components/core/FontFrame/Settings/Select";
const STYLES_LABEL = (theme) => css`
font-size: 0.875rem;

View File

@ -4,7 +4,7 @@ import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import { ContentControl } from "./Controls";
import { ContentControl } from "~/components/core/FontFrame/Settings/Controls";
const CONTROLS_STYLES_WRAPPER = (theme) => css`
width: fit-content;

View File

@ -3,7 +3,12 @@ import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import { Controller, AlignmentControl, ContentControl, SettingsControl } from "./Controls";
import {
Controller,
AlignmentControl,
ContentControl,
SettingsControl,
} from "~/components/core/FontFrame/Settings/Controls";
const STYLES_CONTROLLER_WRAPPER = (theme) =>
css`

View File

@ -1,6 +1,6 @@
import * as React from "react";
import ContentEditable from "./ContentEditable";
import ContentEditable from "~/components/core/FontFrame/Views/ContentEditable";
import { css } from "@emotion/react";

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { css } from "@emotion/react";
import ContentEditable from "./ContentEditable";
import ContentEditable from "~/components/core/FontFrame/Views/ContentEditable";
const STYLES_SENTENCE_WRAPPER = (theme) => css`
.font_frame_sentence {

View File

@ -1,6 +1,6 @@
import Glyphs from "./Glyphs";
import Sentence from "./Sentence";
import Paragraph from "./Paragraph";
import Glyphs from "~/components/core/FontFrame/Views/Glyphs";
import Sentence from "~/components/core/FontFrame/Views/Sentence";
import Paragraph from "~/components/core/FontFrame/Views/Paragraph";
export default function FontView({
settings,

View File

@ -1,6 +1,6 @@
import * as React from "react";
import * as Events from "~/common/custom-events";
import * as Content from "./Views/content";
import * as Content from "~/components/core/FontFrame/Views/content";
import * as Strings from "~/common/strings";
import { generateNumberByStep } from "~/common/utilities";

View File

@ -3,10 +3,13 @@ import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import { useFont, useFontControls } from "./hooks";
import { Controls } from "./Settings/index";
import { FixedControls, MobileFixedControls } from "./Settings/FixedControls";
import FontView from "./Views/index";
import { useFont, useFontControls } from "~/components/core/FontFrame/hooks";
import { Controls } from "~/components/core/FontFrame/Settings/index";
import {
FixedControls,
MobileFixedControls,
} from "~/components/core/FontFrame/Settings/FixedControls";
import FontView from "~/components/core/FontFrame/Views/index";
const GET_STYLES_CONTAINER = (theme) => css`
position: relative;

View File

@ -77,7 +77,7 @@ const STYLES_META = (theme) => css`
margin-bottom: 12px;
`;
const STYLES_DEVIDER = (theme) => css`
const STYLES_DIVIDER = (theme) => css`
position: sticky;
// Note(Amine): asset padding
top: -120px;
@ -155,7 +155,7 @@ export default function MarkdownFrame({ url, date }) {
<div css={STYLES_META}>
<span>{Strings.toDate(date)}</span> / <span>{readTime} min read</span>
</div>
<div css={STYLES_DEVIDER} style={{ height: extendScroll ? "4px" : "1px" }}>
<div css={STYLES_DIVIDER} style={{ height: extendScroll ? "4px" : "1px" }}>
<div css={STYLE_PROGRESS} ref={meterRef} style={{ opacity: opacity }} />
<div css={STYLES_INTENT} />
</div>

View File

@ -0,0 +1,240 @@
import React, { useState, useEffect } from "react";
import { css } from "@emotion/react";
import { Logo } from "~/common/logo.js";
import { Link } from "~/components/core/Link";
import * as Constants from "~/common/constants";
const STYLES_ROOT = css`
position: -webkit-sticky;
position: sticky;
top: 0;
padding: 24px 64px;
width: 100%;
height: 100%;
margin: 0 auto;
mix-blend-mode: difference;
z-index: ${Constants.zindex.header};
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 16px 24px;
mix-blend-mode: normal;
}
`;
const STYLES_CONTAINER = css`
max-width: 1440px;
margin: 0 auto;
font-family: ${Constants.font.text};
font-size: 1rem;
width: 100%;
display: flex;
justify-content: space-between;
@media (max-width: ${Constants.sizes.mobile}px) {
display: flex;
justify-content: space-between;
}
`;
const STYLES_LINK = css`
color: ${Constants.system.darkGray};
text-decoration: none;
transition: 200ms ease color;
:hover {
color: ${Constants.system.newBlue};
}
@media (max-width: ${Constants.sizes.mobile}px) {
color: ${Constants.system.slate};
}
`;
const STYLES_LEFT = css`
flex-shrink: 0;
height: 24px;
`;
const STYLES_RIGHT = css`
min-width: 10%;
width: 100%;
display: flex;
justify-content: flex-end;
@media (max-width: ${Constants.sizes.mobile}px) {
display: none;
}
`;
const STYLES_MOBILENAV = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
display: flex;
}
`;
const STYLES_BURGER = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
z-index: ${Constants.zindex.modal};
position: absolute;
top: 12px;
right: 24px;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 24px;
height: 24px;
background: transparent;
border: none;
cursor: pointer;
}
`;
const STYLES_BURGER_BUN = css`
width: 20px;
height: 2px;
background: ${Constants.system.darkGray};
transition: all 0.2s linear;
position: relative;
transform-origin: 1.5px;
transform: rotate(0);
transistion-property: transform;
@media (max-width: ${Constants.sizes.mobile}px) {
background: ${Constants.system.slate};
}
`;
const openBurgerBun = {
transform: `rotate(45deg)`,
};
const STYLES_BURGER_BUN2 = css`
width: 20px;
height: 2px;
background: ${Constants.system.darkGray};
transition: all 0.2s linear;
position: relative;
transform-origin: 1.5px;
transform: rotate(0);
transistion-property: transform;
@media (max-width: ${Constants.sizes.mobile}px) {
background: ${Constants.system.slate};
}
`;
const openBurgerBun2 = {
transform: `rotate(-45deg)`,
};
const STYLES_MENU = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
display: none;
flex-direction: column;
justify-content: center;
background: ${Constants.system.wall};
height: 100vh;
width: 100vw;
text-align: left;
padding: 24px;
position: absolute;
top: 0;
right: 0;
transform: translateX(100%);
transition: 200ms ease-in-out;
transition-property: transform, width;
}
`;
const STYLES_NAVLINK = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 8px 0;
color: ${Constants.system.slate};
text-decoration: none;
transition: color 0.3s linear;
transition-property: transform;
font-family: ${Constants.font.medium};
font-weight: 400;
font-size: ${Constants.typescale.lvl2};
letter-spacing: -0.017rem;
line-height: 1.3;
text-align: left;
:hover {
color: ${Constants.system.darkGray};
}
}
`;
const openMenu = {
display: `flex`,
transform: `translateX(0)`,
};
const openNavLink = {
display: `flex`,
};
const NewWebsitePrototypeHeader = (props) => {
const [open, setOpen] = useState(false);
useEffect(() => {
window.addEventListener("resize", handleOpen);
return () => window.removeEventListener("resize", handleOpen);
});
const handleOpen = () => {
setOpen(false);
};
const communityURL = "https://github.com/filecoin-project/slate";
const signInURL = "/_/auth";
const styleMenu = open ? openMenu : null;
const styleBurgerBun = open ? openBurgerBun : null;
const styleBurgerBun2 = open ? openBurgerBun2 : null;
const styleNavLink = open ? openNavLink : null;
return (
<div css={STYLES_ROOT}>
<div css={STYLES_CONTAINER} style={props.style}>
<div css={STYLES_LEFT}>
<a css={STYLES_LINK} href="/" style={{ marginRight: 24 }}>
<Logo style={{ height: 20 }} />
</a>
</div>
<div css={STYLES_RIGHT}>
<a css={STYLES_LINK} style={{ marginRight: 24 }} href={communityURL}>
Get involved
</a>
<a css={STYLES_LINK} href={signInURL}>
Sign in
</a>
</div>
<div css={STYLES_MOBILENAV}>
<div onClick={() => setOpen(!open)} css={STYLES_BURGER}>
<div css={STYLES_BURGER_BUN} style={styleBurgerBun} />
<div css={STYLES_BURGER_BUN2} style={styleBurgerBun2} />
</div>
<div css={STYLES_MENU} style={styleMenu}>
<a css={STYLES_NAVLINK} style={styleNavLink} href={communityURL}>
Get involved
</a>
<a css={STYLES_NAVLINK} style={styleNavLink} href={signInURL}>
Sign in
</a>
</div>
</div>
</div>
</div>
);
};
export default NewWebsitePrototypeHeader;

View File

@ -23,7 +23,7 @@ const coordsCollide = (aTop, aLeft, bTop, bLeft, aWidth, aHeight, bWidth, bHeigh
);
};
const doCoordsCollide = (a, b, tolerance = 0) => {
export default function DoObjectsCollide(a, b, tolerance = 0) {
const aObj = a instanceof HTMLElement ? getBoundsForNode(a) : a;
const bObj = b instanceof HTMLElement ? getBoundsForNode(b) : b;
@ -38,6 +38,4 @@ const doCoordsCollide = (a, b, tolerance = 0) => {
bObj.offsetHeight,
tolerance
);
};
export default doCoordsCollide;
}

View File

@ -2,7 +2,7 @@ import * as React from "react";
import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import doObjectsCollide from "./doObjectsCollide";
import doObjectsCollide from "~/components/core/Selectable/doObjectsCollide";
const Context = React.createContext();

View File

@ -1,2 +1,2 @@
export { default as GroupSelectable } from "./groupSelectable";
export { default as Selectable } from "./selectable";
export { default as GroupSelectable } from "~/components/core/Selectable/groupSelectable";
export { default as Selectable } from "~/components/core/Selectable/selectable";

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { useSelectable } from "./groupSelectable";
import { useSelectable } from "~/components/core/Selectable/groupSelectable";
export default function Selectable({ children, selectableKey, style, ...props }) {
const ref = React.useRef();

View File

@ -146,8 +146,7 @@ export class SignIn extends React.Component {
password: this.state.password,
});
}
if (!Events.hasError(response)) {
if (response && !response.error) {
window.location.replace("/_/activity");
}
this.setState({ loading: false });
@ -273,8 +272,10 @@ export class SignIn extends React.Component {
onChange={this._handleChange}
onSubmit={this._handleSubmit}
style={{ paddingRight: 42 }}
onClickIcon={() => this.setState({ showPassword: !this.state.showPassword })}
icon={this.state.showPassword ? SVG.EyeOff : SVG.Eye}
/>
<div
{/* <div
style={{ position: "absolute", right: 2, top: 2, padding: 8, cursor: "pointer" }}
onClick={() => this.setState({ showPassword: !this.state.showPassword })}
>
@ -291,7 +292,7 @@ export class SignIn extends React.Component {
style={{ color: Constants.system.grayBlack }}
/>
)}
</div>
</div> */}
</div>
<System.CheckBox
@ -323,7 +324,7 @@ export class SignIn extends React.Component {
this.setState({ scene: "SIGN_IN", loading: false });
}}
>
Already have an account?
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Already have an account?
</div>
</div>
</React.Fragment>
@ -371,6 +372,8 @@ export class SignIn extends React.Component {
value={this.state.password}
onChange={this._handleChange}
onSubmit={this._handleSubmit}
onClickIcon={() => this.setState({ showPassword: !this.state.showPassword })}
icon={this.state.showPassword ? SVG.EyeOff : SVG.Eye}
/>
<System.ButtonPrimary
@ -389,7 +392,8 @@ export class SignIn extends React.Component {
this.setState({ scene: "CREATE_ACCOUNT", loading: false });
}}
>
Not registered? Sign up instead
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Not registered? Sign up
instead
</div>
</div>
</React.Fragment>

View File

@ -1,57 +1,52 @@
import React, { useState, useEffect } from "react";
import { css } from "@emotion/react";
import { Logo } from "~/common/logo.js";
import { Link } from "~/components/core/Link";
import * as React from "react";
import * as Constants from "~/common/constants";
const STYLES_ROOT = css`
import { Logo } from "~/common/logo.js";
import { css } from "@emotion/react";
const STYLES_CONTAINER = css`
position: -webkit-sticky;
position: sticky;
top: 0;
padding: 24px 64px;
padding: 16px 32px;
width: 100%;
margin: 0 auto;
mix-blend-mode: difference;
z-index: ${Constants.zindex.header};
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 16px 24px;
mix-blend-mode: normal;
}
`;
const STYLES_CONTAINER = css`
max-width: 1440px;
margin: 0 auto;
font-family: ${Constants.font.text};
font-size: 1rem;
font-weight: 400;
font-size: ${Constants.typescale.lvl0};
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
@supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) {
-webkit-backdrop-filter: blur(25px);
backdrop-filter: blur(25px);
background-color: rgba(248, 248, 248, 0.7);
}
@media (max-width: ${Constants.sizes.mobile}px) {
display: flex;
justify-content: space-between;
padding: 12px 24px;
}
`;
const STYLES_LINK = css`
color: ${Constants.system.darkGray};
color: ${Constants.system.grayBlack};
text-decoration: none;
transition: 200ms ease color;
display: block;
display: flex;
align-items: center;
height: 100%;
:hover {
color: ${Constants.system.newBlue};
}
@media (max-width: ${Constants.sizes.mobile}px) {
color: ${Constants.system.slate};
color: ${Constants.system.brand};
}
`;
const STYLES_LEFT = css`
flex-shrink: 0;
height: 24px;
height: 100%;
`;
const STYLES_RIGHT = css`
@ -59,179 +54,25 @@ const STYLES_RIGHT = css`
width: 100%;
display: flex;
justify-content: flex-end;
@media (max-width: ${Constants.sizes.mobile}px) {
display: none;
}
text-align: left;
`;
const STYLES_MOBILENAV = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
display: flex;
}
`;
const STYLES_BURGER = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
z-index: ${Constants.zindex.modal};
position: absolute;
top: 12px;
right: 24px;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 24px;
height: 24px;
background: transparent;
border: none;
cursor: pointer;
}
`;
const STYLES_BURGER_BUN = css`
width: 20px;
height: 2px;
background: ${Constants.system.darkGray};
transition: all 0.2s linear;
position: relative;
transform-origin: 1.5px;
transform: rotate(0);
transistion-property: transform;
@media (max-width: ${Constants.sizes.mobile}px) {
background: ${Constants.system.slate};
}
`;
const openBurgerBun = {
transform: `rotate(45deg)`,
};
const STYLES_BURGER_BUN2 = css`
width: 20px;
height: 2px;
background: ${Constants.system.darkGray};
transition: all 0.2s linear;
position: relative;
transform-origin: 1.5px;
transform: rotate(0);
transistion-property: transform;
@media (max-width: ${Constants.sizes.mobile}px) {
background: ${Constants.system.slate};
}
`;
const openBurgerBun2 = {
transform: `rotate(-45deg)`,
};
const STYLES_MENU = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
display: none;
flex-direction: column;
justify-content: center;
background: ${Constants.system.wall};
height: 100vh;
width: 100vw;
text-align: left;
padding: 24px;
position: absolute;
top: 0;
right: 0;
transform: translateX(100%);
transition: 200ms ease-in-out;
transition-property: transform, width;
}
`;
const STYLES_NAVLINK = css`
display: none;
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 8px 0;
color: ${Constants.system.slate};
text-decoration: none;
transition: color 0.3s linear;
transition-property: transform;
font-family: ${Constants.font.medium};
font-weight: 400;
font-size: ${Constants.typescale.lvl2};
letter-spacing: -0.017rem;
line-height: 1.3;
text-align: left;
:hover {
color: ${Constants.system.darkGray};
}
}
`;
const openMenu = {
display: `flex`,
transform: `translateX(0)`,
};
const openNavLink = {
display: `flex`,
};
const WebsitePrototypeHeader = (props) => {
const [open, setOpen] = useState(false);
useEffect(() => {
window.addEventListener("resize", handleOpen);
return () => window.removeEventListener("resize", handleOpen);
});
const handleOpen = () => {
setOpen(false);
};
const communityURL = "https://github.com/filecoin-project/slate";
const signInURL = "/_/auth?tab=signin";
const styleMenu = open ? openMenu : null;
const styleBurgerBun = open ? openBurgerBun : null;
const styleBurgerBun2 = open ? openBurgerBun2 : null;
const styleNavLink = open ? openNavLink : null;
return (
<div css={STYLES_ROOT}>
<div css={STYLES_CONTAINER} style={props.style}>
<div css={STYLES_LEFT}>
<a css={STYLES_LINK} href="/" style={{ marginRight: 24 }}>
<Logo style={{ height: 20 }} />
</a>
</div>
<div css={STYLES_RIGHT}>
<a css={STYLES_LINK} style={{ marginRight: 24 }} href={communityURL}>
Get involved
</a>
<a css={STYLES_LINK} href={signInURL}>
Sign in
</a>
</div>
<div css={STYLES_MOBILENAV}>
<div onClick={() => setOpen(!open)} css={STYLES_BURGER}>
<div css={STYLES_BURGER_BUN} style={styleBurgerBun} />
<div css={STYLES_BURGER_BUN2} style={styleBurgerBun2} />
</div>
<div css={STYLES_MENU} style={styleMenu}>
<a css={STYLES_NAVLINK} style={styleNavLink} href={communityURL}>
Get involved
</a>
<a css={STYLES_NAVLINK} style={styleNavLink} href={signInURL}>
Use Slate
</a>
</div>
</div>
<div css={STYLES_CONTAINER} style={props.style}>
<div css={STYLES_LEFT}>
<a css={STYLES_LINK} href="/" style={{ marginRight: 16, position: "relative", top: "1px" }}>
<Logo style={{ height: 20 }} />
</a>
</div>
{/* <div css={STYLES_RIGHT}>
<a css={STYLES_LINK} href="/_" style={{ marginRight: 24 }}>
Sign up
</a>
<a css={STYLES_LINK} href="/_">
Sign in
</a>
</div> */}
</div>
);
};

View File

@ -0,0 +1,104 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as SVGLogo from "~/common/logo";
import { css } from "@emotion/react";
const STYLES_ROOT = css`
padding: 24px 88px 24px 64px;
width: 100%;
margin: 0 auto;
z-index: ${Constants.zindex.header};
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 16px 24px;
}
`;
const STYLES_CONTAINER = css`
max-width: 1440px;
margin: 0 auto;
font-family: ${Constants.font.text};
font-weight: 400;
font-size: ${Constants.typescale.lvl1};
width: 100%;
`;
const STYLES_NAV_CONTAINER = css`
display: flex;
justify-content: space-between;
margin-bottom: 8px;
`;
const STYLES_LINK = css`
color: ${Constants.system.grayBlack};
text-decoration: none;
transition: 200ms ease color;
text-align: left;
display: block;
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_PARAGRAPH = css`
font-family: ${Constants.font.text};
color: ${Constants.system.pitchBlack};
text-decoration: none;
transition: 200ms ease color;
overflow-wrap: break-word;
max-width: 50%;
min-width: 10%;
text-align: left;
margin-left: 30px;
display: block;
@media (max-width: ${Constants.sizes.mobile}px) {
max-width: 100%;
}
`;
const STYLES_LEFT = css`
flex-shrink: 0;
display: flex;
max-width: 70%;
`;
const STYLES_RIGHT = css`
min-width: 10%;
width: 100%;
display: flex;
justify-content: flex-end;
text-align: left;
`;
const WebsitePrototypeHeaderGeneric = (props) => {
return (
<div css={STYLES_ROOT}>
<div css={STYLES_CONTAINER}>
<div css={STYLES_NAV_CONTAINER} style={props.style}>
<div css={STYLES_LEFT}>
<a css={STYLES_LINK} href={props.href} style={{ marginRight: 12 }}>
<SVGLogo.Symbol height={`20px`} style={{ transform: "translateY(-1px)" }} />
</a>
<a css={STYLES_LINK} href={props.href}>
{props.title}
</a>
</div>
<div css={STYLES_RIGHT}>
<a css={STYLES_LINK} href="/_/auth?tab=signup" style={{ marginRight: 24 }}>
Sign up
</a>
<a css={STYLES_LINK} href="/_/auth?tab=signin">
Sign in
</a>
</div>
</div>
<div css={STYLES_PARAGRAPH}>{props.children}</div>
</div>
</div>
);
};
export default WebsitePrototypeHeaderGeneric;

View File

@ -26,7 +26,7 @@ const STYLES_BUTTON = `
const STYLES_BUTTON_PRIMARY = css`
${STYLES_BUTTON}
cursor: pointer;
background-color: ${Constants.system.brand};
background-color: ${Constants.system.blue};
color: ${Constants.system.white};
:hover {

View File

@ -89,9 +89,9 @@ export class CheckBox extends React.Component {
render() {
return (
<label css={STYLES_CHECKBOX} style={this.props.style}>
<label css={[STYLES_CHECKBOX, this.props.containerStyles]} style={this.props.style}>
<figure
css={STYLES_CHECKBOX_FIGURE}
css={[STYLES_CHECKBOX_FIGURE, this.props.inputStyles]}
style={
this.props.value
? {
@ -116,7 +116,7 @@ export class CheckBox extends React.Component {
checked={this.props.value}
onChange={() => this._handleChange(this.props.value)}
/>
<span css={STYLES_CHECKBOX_LABEL}>{this.props.children}</span>
<span css={[STYLES_CHECKBOX_LABEL, this.props.labelStyles]}>{this.props.children}</span>
</label>
);
}

View File

@ -0,0 +1,15 @@
import * as React from "react";
export const Divider = ({ width = "100%", height = "1px", color, ...props }) => {
return (
<div
css={(theme) => ({
height,
width,
minHeight: height,
backgroundColor: theme.system?.[color] || color,
})}
{...props}
/>
);
};

View File

@ -4,24 +4,24 @@ import * as SVG from "~/common/svg";
import * as Strings from "~/common/strings";
import { css } from "@emotion/react";
import { P } from "~/components/system";
import { DescriptionGroup } from "~/components/system/components/fragments/DescriptionGroup";
const INPUT_STYLES = `
const INPUT_STYLES = css`
box-sizing: border-box;
font-family: ${Constants.font.text};
-webkit-appearance: none;
width: 100%;
height: 40px;
background: ${Constants.system.white};
background: transparent;
color: ${Constants.system.black};
border-radius: 4px;
display: flex;
font-size: 14px;
align-items: center;
justify-content: flex-start;
border-radius: 4px;
padding: 0 16px 0 16px;
outline: 0;
border: 0;
border: none;
box-sizing: border-box;
transition: 200ms ease all;
`;
@ -31,7 +31,8 @@ const STYLES_UNIT = css`
font-size: 14px;
color: ${Constants.system.darkGray};
position: absolute;
top: 12px;
top: 50%;
transform: translateY(-50%);
right: 24px;
`;
@ -51,16 +52,19 @@ const STYLES_INPUT_CONTAINER_FULL = css`
`;
const STYLES_INPUT = css`
${INPUT_STYLES}
padding: 0 16px 0 16px;
${"" /* ${INPUT_STYLES} */}
display: flex;
align-items: center;
justify-content: flex-start;
height: 40px;
border-radius: 4px;
background: ${Constants.system.white};
text-overflow: ellipsis;
white-space: nowrap;
box-shadow: 0 0 0 1px ${Constants.system.gray30} inset;
border: 1px solid ${Constants.system.gray30};
:focus {
outline: 0;
border: 0;
}
::placeholder {
@ -86,14 +90,24 @@ const STYLES_ICON = css`
right: 12px;
margin-top: 1px;
bottom: 12px;
top: 50%;
transform: translateY(-50%);
transition: 200ms ease all;
cursor: pointer;
color: ${Constants.system.grayBlack};
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_PIN_INPUT = (theme) => css`
text-align: center;
height: 50px;
padding: 0;
font-family: ${theme.font.medium};
font-size: ${theme.typescale.lvl2};
`;
const INPUT_COLOR_MAP = {
SUCCESS: Constants.system.green,
ERROR: Constants.system.red,
@ -103,6 +117,7 @@ const INPUT_COLOR_MAP = {
export class Input extends React.Component {
_unit;
_input;
_isPin = this.props.type === "pin";
componentDidMount = () => {
if (this.props.unit) {
@ -119,13 +134,40 @@ export class Input extends React.Component {
document.execCommand("copy");
};
_formatPin = (pin) => {
let formattedPin = pin.replace(/[\D\s\._\-]+/g, "");
if (formattedPin.length > 3)
formattedPin = formattedPin.slice(0, 3) + " " + formattedPin.slice(3);
if (formattedPin.length > 7) formattedPin = formattedPin.slice(0, 7);
return formattedPin;
};
_parsePin = (pin) => {
let parsedPin = pin.replace(/[\D\s\._\-]+/g, "");
if (parsedPin.length > 7) parsedPin = parsedPin.slice(0, 7);
return parsedPin;
};
_handleSubmit = (e) => {
if (this._isPin) {
let code = this.props.value.replace(/[\D\s\._\-]+/g, "");
code = code.slice(0, 7);
this.props.onSubmit(code);
return;
}
this.props.onSubmit(e);
};
_handleKeyUp = (e) => {
if (this.props.onKeyUp) {
this.props.onKeyUp(e);
}
if ((e.which === 13 || e.keyCode === 13) && this.props.onSubmit) {
this.props.onSubmit(e);
this._handleSubmit(e);
return;
}
};
@ -144,6 +186,13 @@ export class Input extends React.Component {
return;
}
if (this._isPin) {
const pin = e.target.value;
e.target.value = this._parsePin(pin);
this.props.onChange(e);
return;
}
if (this.props.onChange) {
this.props.onChange(e);
}
@ -151,66 +200,81 @@ export class Input extends React.Component {
render() {
return (
<div
css={this.props.full ? STYLES_INPUT_CONTAINER_FULL : STYLES_INPUT_CONTAINER}
style={this.props.containerStyle}
>
<DescriptionGroup
full={this.props.full}
tooltip={this.props.tooltip}
label={this.props.label}
style={this.props.descriptionStyle}
description={this.props.description}
/>
<div style={{ position: "relative" }}>
<input
ref={(c) => {
this._input = c;
}}
css={STYLES_INPUT}
autoFocus={this.props.autoFocus}
value={this.props.value}
name={this.props.name}
type={this.props.type}
placeholder={this.props.placeholder}
onChange={this._handleChange}
onFocus={
this.props.autoHighlight
? () => {
this._input.select();
}
: this.props.onFocus
}
onBlur={this.props.onBlur}
onKeyUp={this._handleKeyUp}
autoComplete="off"
disabled={this.props.disabled}
readOnly={this.props.readOnly}
<>
<div
css={this.props.full ? STYLES_INPUT_CONTAINER_FULL : STYLES_INPUT_CONTAINER}
style={this.props.containerStyle}
>
<DescriptionGroup
full={this.props.full}
tooltip={this.props.tooltip}
label={this.props.label}
labelStyle={{ fontSize: Constants.typescale.lvl0 }}
style={this.props.descriptionStyle}
description={this.props.description}
/>
<div
css={[STYLES_INPUT, this.props.inputCss]}
style={{
width: "100%",
position: "relative",
boxShadow: this.props.validation
? `0 1px 4px rgba(0, 0, 0, 0.07), inset 0 0 0 2px ${
INPUT_COLOR_MAP[this.props.validation]
}`
: null,
paddingRight: this.props.copyable || this.props.icon ? "32px" : "24px",
...this.props.style,
}}
/>
<div
css={STYLES_UNIT}
ref={(c) => {
this._unit = c;
}}
>
{this.props.unit}
<input
ref={(c) => {
this._input = c;
}}
css={[INPUT_STYLES, this._isPin && STYLES_PIN_INPUT]}
autoFocus={this.props.autoFocus}
value={this._isPin ? this._formatPin(this.props.value) : this.props.value}
name={this.props.name}
type={this._isPin ? "text" : this.props.type}
placeholder={this.props.placeholder}
onChange={this._handleChange}
onFocus={
this.props.autoHighlight
? () => {
this._input.select();
}
: this.props.onFocus
}
onBlur={this.props.onBlur}
onKeyUp={this._handleKeyUp}
autoComplete="off"
disabled={this.props.disabled}
readOnly={this.props.readOnly}
required={this.props.required}
style={{
width: this.props.copyable || this.props.icon ? "calc(100% - 16px)" : "100%",
}}
/>
<div
css={STYLES_UNIT}
ref={(c) => {
this._unit = c;
}}
>
{this.props.unit}
</div>
{this.props.unit ? null : this.props.icon ? (
<this.props.icon
height="16px"
css={STYLES_ICON}
style={{ cursor: (this.props.onClickIcon || this.props.onSubmit) && "pointer" }}
onClick={this.props.onClickIcon || this.props.onSubmit || this._handleSubmit}
/>
) : this.props.copyable ? (
<SVG.CopyAndPaste height="16px" css={STYLES_ICON} onClick={this._handleCopy} />
) : null}
</div>
</div>
{this.props.unit ? null : this.props.icon ? (
<this.props.icon height="16px" css={STYLES_ICON} onClick={this.props.onSubmit} />
) : this.props.copyable ? (
<SVG.CopyAndPaste height="16px" css={STYLES_ICON} onClick={this._handleCopy} />
) : null}
</div>
</>
);
}
}

View File

@ -63,6 +63,7 @@ import { Table } from "~/components/system/components/Table";
import { Textarea } from "~/components/system/components/Textarea";
import { Toggle } from "~/components/system/components/Toggle";
import { H1, H2, H3, H4, P, UL, OL, LI } from "~/components/system/components/Typography";
import { Divider } from "~/components/system/components/Divider";
// NOTE(jim): Fragments
import { Boundary } from "~/components/system/components/fragments/Boundary";
@ -137,6 +138,7 @@ export {
Table,
Textarea,
Toggle,
Divider,
Tag,
H1,
H2,

View File

@ -15,6 +15,9 @@ export const TEXTILE_BUCKET_LIMIT = TEXTILE_ACCOUNT_BYTE_LIMIT - 234;
// NOTE(jim): 100mb
export const MIN_ARCHIVE_SIZE_BYTES = 104857600;
// NOTE(amine): 15 minutes
export const TOKEN_EXPIRATION_TIME = 2 * 60 * 60 * 1000;
export const slateProperties = [
"slates.id",
"slates.slatename",

View File

@ -5,6 +5,14 @@ import updateUserById from "~/node_common/data/methods/update-user-by-id";
import deleteUserById from "~/node_common/data/methods/delete-user-by-id";
import getUserByUsername from "~/node_common/data/methods/get-user-by-username";
import getUserById from "~/node_common/data/methods/get-user-by-id";
import getUserByEmail from "~/node_common/data/methods/get-user-by-email";
import getUserByTwitterId from "~/node_common/data/methods/get-user-by-twitter-id";
// NOTE(amine)
// TwitterTokens postgres queries
import createTwitterToken from "~/node_common/data/methods/create-twitter-token";
import getTwitterToken from "~/node_common/data/methods/get-twitter-token";
import updateTwitterToken from "~/node_common/data/methods/update-twitter-token";
// NOTE(martina):
// File postgres queries
@ -77,6 +85,16 @@ import getEverySlate from "~/node_common/data/methods/get-every-slate";
import getEveryUser from "~/node_common/data/methods/get-every-user";
import getEveryFile from "~/node_common/data/methods/get-every-file";
// NOTE(toast):
// Verification sessions for email verif
import createVerification from "~/node_common/data/methods/create-verification";
import updateVerification from "~/node_common/data/methods/update-verification";
import deleteVerificationByEmail from "~/node_common/data/methods/delete-verification-by-email";
import deleteVerificationBySid from "~/node_common/data/methods/delete-verification-by-sid";
import getVerificationByEmail from "~/node_common/data/methods/get-verification-by-email";
import getVerificationBySid from "~/node_common/data/methods/get-verification-by-sid";
import pruneVerifications from "~/node_common/data/methods/prune-verifications";
// NOTE(jim):
// one-offs
import createOrphan from "~/node_common/data/methods/create-orphan";
@ -90,6 +108,8 @@ export {
deleteUserById,
getUserByUsername,
getUserById,
getUserByEmail,
getUserByTwitterId,
//NOTE(martina): File operations
createFile,
getFileByCid,
@ -145,4 +165,16 @@ export {
getEverySlate,
getEveryUser,
getEveryFile,
//NOTE(toast): Verification operations
createVerification,
getVerificationByEmail,
getVerificationBySid,
deleteVerificationByEmail,
deleteVerificationBySid,
pruneVerifications,
updateVerification,
// NOTE(amine): Twitter
createTwitterToken,
getTwitterToken,
updateTwitterToken,
};

View File

@ -0,0 +1,24 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ token, tokenSecret }) => {
return await runQuery({
label: "CREATE_TWITTER_TOKEN",
queryFn: async (DB) => {
const query = await DB.insert({
token,
tokenSecret,
})
.into("twitterTokens")
.returning("*");
const index = query ? query.pop() : null;
return JSON.parse(JSON.stringify(index));
},
errorFn: async (e) => {
return {
error: true,
decorator: "CREATE_TWITTER_TOKEN",
};
},
});
};

View File

@ -1,6 +1,6 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ password, username, salt, data = {} }) => {
export default async ({ password, username, email, salt, twitterId, data = {} }) => {
return await runQuery({
label: "CREATE_USER",
queryFn: async (DB) => {
@ -9,6 +9,9 @@ export default async ({ password, username, salt, data = {} }) => {
salt,
data,
username,
email,
twitterId,
authVersion: 2,
})
.into("users")
.returning("*");

View File

@ -0,0 +1,29 @@
import { runQuery } from "~/node_common/data/utilities";
//NOTE(toast): allows for creation of mulitple codes and just
//passing the sid for the most recent verification session
export default async ({ email, pin, twitterToken, username, type }) => {
return await runQuery({
label: "CREATE_VERIFICATION",
queryFn: async (DB) => {
const query = await DB.insert({
email,
pin,
username,
twitterToken,
type,
})
.into("verifications")
.returning("*");
const index = query ? query.pop() : null;
return JSON.parse(JSON.stringify(index));
},
errorFn: async (e) => {
return {
error: true,
decorator: "CREATE_VERIFICATION",
};
},
});
};

View File

@ -0,0 +1,18 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ email }) => {
return await runQuery({
label: "DELETE_VERIFICATION_BY_EMAIL",
queryFn: async (DB) => {
const query = await DB.from("verifications").where({ email }).del();
return 1 === query;
},
errorFn: async (e) => {
return {
error: true,
decorator: "DELETE_VERIFICATION_BY_EMAIL",
};
},
});
};

View File

@ -0,0 +1,18 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ sid }) => {
return await runQuery({
label: "DELETE_VERIFICATION_BY_SID",
queryFn: async (DB) => {
const query = await DB.from("verifications").where({ sid }).del();
return 1 === query;
},
errorFn: async (e) => {
return {
error: true,
decorator: "DELETE_VERIFICATION_BY_SID",
};
},
});
};

View File

@ -0,0 +1,26 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ token }) => {
return await runQuery({
label: "GET_TWITTER_TOKEN",
queryFn: async (DB) => {
const query = await DB.select("*").from("twitterTokens").where({ token }).first();
if (!query || query.error) {
return null;
}
if (query.token) {
return JSON.parse(JSON.stringify(query));
}
return null;
},
errorFn: async (e) => {
return {
error: true,
decorator: "GET_TWITTER_TOKEN",
};
},
});
};

View File

@ -0,0 +1,29 @@
import * as Serializers from "~/node_common/serializers";
import { runQuery } from "~/node_common/data/utilities";
//NOTE(toast): should only be used for checking if an email is taken
//ALWAYS sanitize it before sending result to frontend
export default async ({ email, sanitize = false }) => {
return await runQuery({
label: "GET_USER_BY_EMAIL",
queryFn: async (DB) => {
let query = await DB.select("*").from("users").where({ email }).first();
if (!query || query.error) {
return null;
}
if (sanitize) {
query = Serializers.sanitizeUser(query);
}
return JSON.parse(JSON.stringify(query));
},
errorFn: async (e) => {
return {
error: true,
decorator: "GET_USER_BY_EMAIL",
};
},
});
};

View File

@ -0,0 +1,27 @@
import * as Serializers from "~/node_common/serializers";
import { runQuery } from "~/node_common/data/utilities";
//NOTE(toast): should only be used for checking if an email is taken
//ALWAYS sanitize it before sending result to frontend
export default async ({ twitterId }) => {
return await runQuery({
label: "GET_USER_BY_TWITTER_ID",
queryFn: async (DB) => {
let query = await DB.select("*").from("users").where({ twitterId: twitterId }).first();
if (!query || query.error) {
return null;
}
query = Serializers.sanitizeUser(query);
return JSON.parse(JSON.stringify(query));
},
errorFn: async (e) => {
return {
error: true,
decorator: "GET_USER_BY_TWITTER_ID",
};
},
});
};

View File

@ -0,0 +1,26 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ email }) => {
return await runQuery({
label: "GET_VERIFICATION_BY_EMAIL",
queryFn: async (DB) => {
const query = await DB.select("*").from("verifications").where({ email }).first();
if (!query || query.error) {
return null;
}
if (query.id) {
return JSON.parse(JSON.stringify(query));
}
return null;
},
errorFn: async (e) => {
return {
error: true,
decorator: "GET_VERIFICATION_BY_EMAIL",
};
},
});
};

View File

@ -0,0 +1,25 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ sid }) => {
return await runQuery({
label: "GET_VERIFICATION_BY_SID",
queryFn: async (DB) => {
const query = await DB.select("*").from("verifications").where({ sid }).first();
if (!query || query.error) {
return null;
}
if (query.id) {
return JSON.parse(JSON.stringify(query));
}
return query;
},
errorFn: async (e) => {
return {
error: true,
decorator: "GET_VERIFICATION_BY_SID",
};
},
});
};

View File

@ -0,0 +1,21 @@
import { runQuery } from "~/node_common/data/utilities";
export default async () => {
return await runQuery({
label: "PRUNE_VERIFICATION",
queryFn: async (DB) => {
const currentTime = new Date();
const cutoffTime = new Date(currentTime.getTime() - 15 * 60000);
//NOTE(toast): removes verification sessions created before 15 min ago
const query = await DB.from("verifications").whereBetween("createdAt", "<", cutoffTime).del();
return query === 1;
},
errorFn: async (e) => {
return {
error: true,
decorator: "PRUNE_VERIFICATION",
};
},
});
};

View File

@ -0,0 +1,27 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ token, email, id, screen_name, verified }) => {
return await runQuery({
label: "UPDATE_TWITTER_TOKEN",
queryFn: async (DB) => {
const response = await DB.from("twitterTokens")
.where("token", token)
.update({
email,
id_str: id,
screen_name,
verified: verified,
})
.returning("*");
const index = response ? response.pop() : null;
return JSON.parse(JSON.stringify(index));
},
errorFn: async (e) => {
return {
error: true,
decorator: "UPDATE_TWITTER_TOKEN",
};
},
});
};

View File

@ -2,7 +2,17 @@ import * as Serializers from "~/node_common/serializers";
import { runQuery } from "~/node_common/data/utilities";
export default async ({ id, data, lastActive, username, salt, password }) => {
export default async ({
id,
data,
lastActive,
username,
email,
salt,
password,
twitterId,
authVersion,
}) => {
const updateObject = { id, lastActive: lastActive || new Date() };
if (data) {
@ -13,6 +23,10 @@ export default async ({ id, data, lastActive, username, salt, password }) => {
updateObject.username = username.toLowerCase();
}
if (email) {
updateObject.email = email.toLowerCase();
}
if (salt) {
updateObject.salt = salt;
}
@ -21,6 +35,14 @@ export default async ({ id, data, lastActive, username, salt, password }) => {
updateObject.password = password;
}
if (twitterId) {
updateObject.twitterId = twitterId;
}
if (authVersion) {
updateObject.authVersion = authVersion;
}
return await runQuery({
label: "UPDATE_USER_BY_ID",
queryFn: async (DB) => {

View File

@ -0,0 +1,25 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ sid, isVerified, passwordChanged }) => {
return await runQuery({
label: "UPDATE_VERIFICATION",
queryFn: async (DB) => {
const response = await DB.from("verifications")
.where("sid", sid)
.update({
isVerified,
passwordChanged,
})
.returning("*");
const index = response ? response.pop() : null;
return JSON.parse(JSON.stringify(index));
},
errorFn: async (e) => {
return {
error: true,
decorator: "UPDATE_VERIFICATION",
};
},
});
};

View File

@ -20,9 +20,7 @@ export const PUBSUB_SECRET = process.env.PUBSUB_SECRET;
export const ALLOWED_HOST = process.env.ALLOWED_HOST;
export const LOCAL_PASSWORD_ROUNDS_MANUAL = process.env.LOCAL_PASSWORD_ROUNDS_MANUAL;
export const LOCAL_PASSWORD_ROUNDS = process.env.LOCAL_PASSWORD_ROUNDS;
export const LOCAL_PASSWORD_SECRET = `$2b$${LOCAL_PASSWORD_ROUNDS}$${
process.env.LOCAL_PASSWORD_SECRET
}`;
export const LOCAL_PASSWORD_SECRET = `$2b$${LOCAL_PASSWORD_ROUNDS}$${process.env.LOCAL_PASSWORD_SECRET}`;
// NOTE(jim): Custom avatars
export const AVATAR_SLATE_ID = process.env.AVATAR_SLATE_ID;
@ -41,4 +39,13 @@ export const TEXTILE_SLACK_WEBHOOK_KEY = process.env.TEXTILE_SLACK_WEBHOOK_KEY;
export const RESOURCE_URI_UPLOAD = process.env.RESOURCE_URI_UPLOAD;
export const RESOURCE_URI_STORAGE_UPLOAD = process.env.RESOURCE_URI_STORAGE_UPLOAD;
export const RESOURCE_URI_PUBSUB = process.env.RESOURCE_URI_PUBSUB;
export const RESOURCE_URI_SEARCH = process.env.RESOURCE_URI_SEARCH;
export const RESOURCE_URI_SEARCH = process.env.RESOURCE_URI_SEARCH;
//NOTE(amine): Twitter
export const TWITTER_API_KEY = process.env.TWITTER_API_KEY;
export const TWITTER_SECRET_API_KEY = process.env.TWITTER_SECRET_API_KEY;
export const TWITTER_CALLBACK = process.env.TWITTER_CALLBACK;
//NOTE(toast): Sendgrid
export const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY;
export const SENDGRID_WEBHOOK_KEY = process.env.SENDGRID_WEBHOOK_KEY;

View File

@ -0,0 +1,72 @@
import * as Environment from "~/node_common/environment";
import sgMail from "@sendgrid/mail";
import "isomorphic-fetch";
sgMail.setApiKey(Environment.SENDGRID_API_KEY);
//NOTE(toast): please see https://sendgrid.com/docs/api-reference/
//for sendgrid request structure, see what's optional
//also see https://github.com/sendgrid/sendgrid-nodejs/tree/main/packages/mail
export const sendEmail = async ({
personalizations,
to,
from,
subject,
content,
optionalData = {},
}) => {
const msg = {
personalizations: personalizations,
to: to,
from: from,
subject: subject,
content: content,
optionalData: optionalData,
};
try {
await sgMail.send(msg);
} catch (error) {
return { decorator: "SEND_EMAIL_FAILURE", error: true };
}
};
//NOTE(toast): templates override content, subject, etc.
//properties that are defined in the template take priority
export const sendTemplate = async ({ to, from, templateId, templateData }) => {
const msg = {
to: to,
from: from,
templateId: templateId,
dynamic_template_data: { ...templateData },
};
try {
await sgMail.send(msg);
} catch (error) {
console.log("SOMETHING", error);
return { decorator: "SEND_TEMPLATE_EMAIL_FAILURE", error: true };
}
};
//NOTE(toast): only available to upgraded sendgrid accounts
//uses their validation service to make sure an email is legit
export const validateEmail = async ({ email }) => {
const msg = { email: email };
const request = await fetch("https://api.sendgrid.com/v3/validations/email", {
method: "POST",
headers: {
Authorization: Environment.SENDGRID_API_KEY,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(msg),
});
try {
await request();
} catch (e) {
return { decorator: "VALIDATE_EMAIL_FAILURE", error: true };
}
};

View File

@ -0,0 +1,73 @@
import * as Environment from "~/node_common/environment";
import { OAuth } from "oauth";
const initiateAuth = () =>
new OAuth(
"https://api.twitter.com/oauth/request_token",
"https://api.twitter.com/oauth/access_token",
Environment.TWITTER_API_KEY,
Environment.TWITTER_SECRET_API_KEY,
"1.0",
Environment.TWITTER_CALLBACK,
"HMAC-SHA1"
);
const getOAuthRequestToken = (OAuthProvider) => () =>
new Promise((resolve, reject) => {
OAuthProvider.getOAuthRequestToken((error, authToken, authSecretToken, results) => {
if (error) {
reject(error);
return;
}
resolve({ authToken, authSecretToken, results });
});
});
const getOAuthAccessToken = (OAuthProvider) => ({ authToken, authSecretToken, authVerifier }) =>
new Promise((resolve, reject) => {
OAuthProvider.getOAuthAccessToken(
authToken,
authSecretToken,
authVerifier,
(error, authAccessToken, authSecretAccessToken, results) => {
if (error) {
reject(error);
return;
}
resolve({ authAccessToken, authSecretAccessToken, results });
}
);
});
const getProtectedResource = (OAuthProvider) => ({
url,
method,
authAccessToken,
authSecretAccessToken,
}) =>
new Promise((resolve, reject) => {
OAuthProvider.getProtectedResource(
url,
method,
authAccessToken,
authSecretAccessToken,
(error, data, response) => {
if (error) {
reject(error);
return;
}
resolve({ data, response });
}
);
});
export const createOAuthProvider = () => {
const auth = initiateAuth();
auth.getProtectedResource;
return {
getOAuthRequestToken: getOAuthRequestToken(auth),
getOAuthAccessToken: getOAuthAccessToken(auth),
getProtectedResource: getProtectedResource(auth),
};
};

View File

@ -11,6 +11,8 @@ export const sanitizeUser = (entity) => {
username: entity.username,
slates: entity.slates, //NOTE(martina): this is not in the database. It is added after
library: entity.library, //NOTE(martina): this is not in the database. It is added after
twitterId: entity.twitterId,
email: entity.email,
data: {
name: entity.data?.name,
photo: entity.data?.photo,
@ -82,6 +84,8 @@ export const cleanUser = (entity) => {
salt: entity.salt,
password: entity.password,
email: entity.email,
twitterId: entity.twitterId,
authVersion: entity.authVersion,
data: entity.data,
// data: {
// name: entity.data?.name,

View File

@ -226,6 +226,9 @@ export const updateStateData = async (state, newState) => {
};
};
export const generateRandomNumberInRange = (min, max) =>
Math.floor(Math.random() * (max - min)) + min;
// NOTE(daniel): get all tags on slates and files
export const getUserTags = ({ library, slates }) => {
let tags = new Set();

4326
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,10 +35,13 @@
"@emotion/babel-preset-css-prop": "11.2.0",
"@emotion/react": "11.1.5",
"@glif/filecoin-number": "^1.1.0-beta.17",
"@sendgrid/client": "^7.4.2",
"@sendgrid/mail": "^7.4.4",
"@slack/webhook": "^6.0.0",
"@textile/hub": "^6.0.2",
"babel-plugin-module-resolver": "^4.1.0",
"bcrypt": "^5.0.0",
"bcryptjs": "^2.4.3",
"blurhash": "^1.1.3",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
@ -47,6 +50,7 @@
"express": "^4.17.1",
"express-rate-limit": "^5.2.5",
"file-saver": "^2.0.5",
"framer-motion": "^4.1.17",
"fs-extra": "^9.1.0",
"heic2any": "0.0.3",
"isomorphic-fetch": "^3.0.0",
@ -59,6 +63,7 @@
"morgan": "^1.10.0",
"next": "^10.0.7",
"next-offline": "^5.0.5",
"oauth": "^0.9.15",
"pg": "^8.5.1",
"prism-react-renderer": "^1.2.0",
"prismjs": "^1.23.0",

View File

@ -14,7 +14,7 @@ export const getServerSideProps = async ({ query }) => {
// },
// };
return {
props: { ...query },
props: { ...JSON.parse(JSON.stringify(query)) },
};
};

102
pages/_/profile[dep].js Normal file
View File

@ -0,0 +1,102 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings";
import * as Events from "~/common/custom-events";
import { css } from "@emotion/react";
import { ButtonPrimary } from "~/components/system/components/Buttons";
import Profile from "~/components/core/Profile";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/WebsitePrototypeFooter";
import CTATransition from "~/components/core/CTATransition";
const DEFAULT_IMAGE =
"https://slate.textile.io/ipfs/bafkreiaow45dlq5xaydaeqocdxvffudibrzh2c6qandpqkb6t3ahbvh6re";
export const getServerSideProps = async (context) => {
return {
props: { ...context.query },
};
};
const STYLES_ROOT = css`
display: block;
grid-template-rows: auto 1fr auto;
font-size: 1rem;
min-height: 100vh;
background-color: ${Constants.system.foreground};
`;
export default class ProfilePage extends React.Component {
state = {
visible: false,
page: null,
};
componentDidMount = () => {
window.onpopstate = this._handleBackForward;
if (!Strings.isEmpty(this.props.cid)) {
let files = this.props.creator.library || [];
let index = files.findIndex((object) => object.cid === this.props.cid);
if (index !== -1) {
Events.dispatchCustomEvent({
name: "slate-global-open-carousel",
detail: { index },
});
}
}
};
_handleBackForward = (e) => {
let page = window.history.state;
this.setState({ page });
Events.dispatchCustomEvent({ name: "slate-global-close-carousel", detail: {} });
};
render() {
const title = this.props.creator
? this.props.creator.data.name
? `${this.props.creator.data.name} on Slate`
: `@${this.props.creator.username} on Slate`
: "404";
const url = `https://slate.host/${title}`;
const description = this.props.creator.data.body;
const image = this.props.creator.data.photo;
if (Strings.isEmpty(image)) {
image = DEFAULT_IMAGE;
}
return (
<WebsitePrototypeWrapper title={title} description={description} url={url} image={image}>
<WebsitePrototypeHeader />
<div css={STYLES_ROOT}>
<Profile
{...this.props}
user={this.props.creator}
page={this.state.page}
isOwner={false}
isAuthenticated={this.props.viewer !== null}
external
/>
</div>
{this.state.visible && (
<div>
<CTATransition
onClose={() => this.setState({ visible: false })}
viewer={this.props.viewer}
open={this.state.visible}
redirectURL={`/_${Strings.createQueryParams({
scene: "NAV_PROFILE",
user: this.props.creator.username,
})}`}
/>
</div>
)}
<WebsitePrototypeFooter />
</WebsitePrototypeWrapper>
);
}
}

375
pages/_/slate[dep].js Normal file
View File

@ -0,0 +1,375 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as System from "~/components/system";
import * as SVG from "~/common/svg";
import * as Strings from "~/common/strings";
import * as Actions from "~/common/actions";
import * as Validations from "~/common/validations";
import * as Events from "~/common/custom-events";
import * as UserBehaviors from "~/common/user-behaviors";
import { ButtonSecondary } from "~/components/system/components/Buttons";
import { css } from "@emotion/react";
import { ViewAllButton } from "~/components/core/ViewAll";
import { SlateLayout } from "~/components/core/SlateLayout";
import { SlateLayoutMobile } from "~/components/core/SlateLayoutMobile";
import { GlobalCarousel } from "~/components/system/components/GlobalCarousel";
import { GlobalModal } from "~/components/system/components/GlobalModal";
import ProcessedText from "~/components/core/ProcessedText";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/WebsitePrototypeFooter";
import CTATransition from "~/components/core/CTATransition";
const DEFAULT_IMAGE =
"https://slate.textile.io/ipfs/bafkreiaow45dlq5xaydaeqocdxvffudibrzh2c6qandpqkb6t3ahbvh6re";
const DEFAULT_BOOK =
"https://slate.textile.io/ipfs/bafkreibk32sw7arspy5kw3p5gkuidfcwjbwqyjdktd5wkqqxahvkm2qlyi";
const DEFAULT_DATA =
"https://slate.textile.io/ipfs/bafkreid6bnjxz6fq2deuhehtxkcesjnjsa2itcdgyn754fddc7u72oks2m";
const DEFAULT_DOCUMENT =
"https://slate.textile.io/ipfs/bafkreiecdiepww52i5q3luvp4ki2n34o6z3qkjmbk7pfhx4q654a4wxeam";
const DEFAULT_VIDEO =
"https://slate.textile.io/ipfs/bafkreibesdtut4j5arclrxd2hmkfrv4js4cile7ajnndn3dcn5va6wzoaa";
const DEFAULT_AUDIO =
"https://slate.textile.io/ipfs/bafkreig2hijckpamesp4nawrhd6vlfvrtzt7yau5wad4mzpm3kie5omv4e";
const STYLES_ROOT = css`
display: block;
min-height: 100vh;
background-color: ${Constants.system.foreground};
padding: 0px 32px 24px 32px;
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 0px;
}
`;
const STYLES_HEADER = css`
padding: 32px 32px 0px 32px;
display: flex;
align-item: baseline;
max-width: 100%;
justify-content: space-between;
@media (max-width: ${Constants.sizes.mobile}px) {
display: block;
padding: 24px 24px 0px 24px;
}
`;
const STYLES_SLATE_INTRO = css`
overflow-wrap: break-word;
white-space: pre-wrap;
flex-shrink: 0;
display: block;
max-width: 85%;
@media (max-width: ${Constants.sizes.mobile}px) {
display: block;
margin-bottom: 24px;
}
`;
const STYLES_TITLELINE = css`
display: flex;
align-items: flex-start;
word-wrap: break-word;
@media (max-width: ${Constants.sizes.mobile}px) {
display: block;
font-size: 14px;
}
`;
const STYLES_CREATOR = css`
flex-shrink: 0;
margin-right: 4px;
text-decoration: none;
font-size: ${Constants.typescale.lvl3};
color: ${Constants.system.black};
font-family: ${Constants.font.medium};
:hover {
color: ${Constants.system.brand};
}
`;
const STYLES_TITLE = css`
font-size: ${Constants.typescale.lvl3};
font-family: ${Constants.font.medium};
font-weight: 400;
color: ${Constants.system.black};
width: auto;
max-width: 100%;
margin-right: 24px;
word-wrap: break-word;
`;
const STYLES_DESCRIPTION = css`
font-size: ${Constants.typescale.lvl0};
color: ${Constants.system.darkGray};
width: 50%;
overflow-wrap: break-word;
white-space: pre-wrap;
margin-top: 16px;
@media (max-width: ${Constants.sizes.mobile}px) {
width: 100%;
}
ul,
ol {
white-space: normal;
}
`;
const STYLES_STATS = css`
font-size: ${Constants.typescale.lvl0};
margin: 16px 0;
display: flex;
width: 100%;
color: ${Constants.system.grayBlack};
`;
const STYLES_STAT = css`
margin-right: 16px;
flex-shrink: 0;
`;
const STYLES_SLATE = css`
padding: 0 32px;
display: block;
width: 100%;
margin: 48px auto 0 auto;
min-height: 10%;
height: 100%;
@media (max-width: ${Constants.sizes.mobile}px) {
padding: 0 24px 0 24px;
}
`;
export const getServerSideProps = async (context) => {
return {
props: { ...context.query },
};
};
export const FileTypeDefaultPreview = (props) => {
if (props.type) {
if (Validations.isVideoType(type)) {
return DEFAULT_VIDEO;
} else if (Validations.isAudioType(type)) {
return DEFAULT_AUDIO;
} else if (Validations.isPdfType(type)) {
return DEFAULT_DOCUMENT;
} else if (Validations.isEpubType(type)) {
return DEFAULT_BOOK;
}
}
return DEFAULT_DATA;
};
export default class SlatePage extends React.Component {
state = {
visible: false,
};
componentDidMount() {
if (!this.props.slate) {
return null;
}
if (!Strings.isEmpty(this.props.cid)) {
let files = this.props.slate.objects || [];
let index = files.findIndex((object) => object.cid === this.props.cid);
if (index !== -1) {
Events.dispatchCustomEvent({
name: "slate-global-open-carousel",
detail: { index },
});
}
}
}
_handleSelect = (index) => {
Events.dispatchCustomEvent({
name: "slate-global-open-carousel",
detail: { index },
});
};
_handleSave = async (layouts) => {
await Actions.updateSlateLayout({
id: this.props.slate.id,
layouts,
});
};
_handleDownloadFiles = () => {
const slateName = this.props.slate.data.name;
const slateFiles = this.props.slate.data.objects;
UserBehaviors.compressAndDownloadFiles({
files: slateFiles,
name: `${slateName}.zip`,
resourceURI: this.props.resources.download,
});
};
render() {
let title = `${this.props.creator.username}/${this.props.slate.slatename}`;
let url = `https://slate.host/${this.props.creator.username}/${this.props.slate.slatename}`;
let headerURL = `https://slate.host/${this.props.creator.username}`;
let { objects, isPublic } = this.props.slate;
let { layouts, body, preview } = this.props.slate.data;
let image;
if (Strings.isEmpty(this.props.cid)) {
image = preview;
if (Strings.isEmpty(image)) {
for (let i = 0; i < objects.length; i++) {
if (
objects[i].data.type &&
Validations.isPreviewableImage(objects[i].data.type) &&
objects[i].data.size &&
objects[i].data.size < Constants.linkPreviewSizeLimit
) {
image = Strings.getURLfromCID(objects[i].cid);
break;
}
}
}
} else {
const cid = Strings.getCIDFromIPFS(each.cid);
let object = objects.find((each) => {
return cid === this.props.cid;
});
if (object) {
title = object.data.name || object.filename;
body = !Strings.isEmpty(object.data.body) ? Strings.elide(object.data.body) : "";
image = object.data.type.includes("image/") ? (
Strings.getURLfromCID(object.cid)
) : (
<FileTypeDefaultPreview type={object.data.type} />
);
url = `${url}/cid:${this.props.cid}`;
}
}
if (Strings.isEmpty(image)) {
image = DEFAULT_IMAGE;
}
const slateCreator = `${this.props.creator.username} / `;
const slateTitle = `${this.props.slate.slatename}`;
const counts = objects.reduce((counts, { ownerId }) => {
counts[ownerId] = (counts[ownerId] || 0) + 1;
return counts;
}, {});
const contributorsCount = Object.keys(counts).length;
const isSlateEmpty = objects.length === 0;
return (
<WebsitePrototypeWrapper title={title} description={body} url={url} image={image}>
<WebsitePrototypeHeader />
<div css={STYLES_ROOT}>
<div css={STYLES_HEADER}>
<div css={STYLES_SLATE_INTRO}>
<div css={STYLES_TITLELINE}>
<a css={STYLES_CREATOR} href={`/${this.props.creator.username}`}>
{slateCreator}
</a>
<div css={STYLES_TITLE}>{slateTitle} </div>
</div>
<div css={STYLES_DESCRIPTION}>
<ViewAllButton fullText={this.props.slate.data.body} maxCharacter={208}>
<ProcessedText text={this.props.slate.data.body} />
</ViewAllButton>
</div>
<div css={STYLES_STATS}>
<div css={STYLES_STAT}>
<div style={{ fontFamily: `${Constants.font.medium}` }}>
{this.props.slate.objects.length}{" "}
<span style={{ color: `${Constants.system.darkGray}` }}>Files</span>
</div>
</div>
{/* <div css={STYLES_STAT}>
<div style={{ fontFamily: `${Constants.font.medium}` }}>
{contributorsCount}{" "}
<span style={{ color: `${Constants.system.darkGray}` }}>Contributors</span>
</div>
</div> */}
</div>
</div>
<div>
{!isSlateEmpty && (
<ButtonSecondary
style={{ marginRight: "16px" }}
onClick={this._handleDownloadFiles}
>
Download
</ButtonSecondary>
)}
<ButtonSecondary onClick={() => this.setState({ visible: true })}>
Follow
</ButtonSecondary>
</div>
</div>
<div css={STYLES_SLATE}>
<GlobalCarousel
data={this.props.slate}
carouselType="SLATE"
viewer={this.props.viewer}
objects={objects}
isMobile={this.props.isMobile}
isOwner={false}
external
/>
{this.props.isMobile ? (
<SlateLayoutMobile
isOwner={false}
items={objects}
fileNames={layouts && layouts.ver === "2.0" ? layouts.fileNames : false}
onSelect={this._handleSelect}
external
/>
) : (
<SlateLayout
external
slateId={this.props.slate.id}
key={this.props.slate.id}
layout={layouts && layouts.ver === "2.0" ? layouts.layout : null}
onSaveLayout={this._handleSave}
isOwner={false}
fileNames={layouts && layouts.ver === "2.0" ? layouts.fileNames : false}
items={objects}
onSelect={this._handleSelect}
defaultLayout={layouts && layouts.ver === "2.0" ? layouts.defaultLayout : true}
creator={this.props.creator}
slate={this.props.slate}
/>
)}
</div>
</div>
<GlobalModal />
{this.state.visible && (
<div>
<CTATransition
onClose={() => this.setState({ visible: false })}
viewer={this.props.viewer}
open={this.state.visible}
redirectURL={`/_${Strings.createQueryParams({
scene: "NAV_SLATE",
user: this.props.creator.username,
slate: this.props.slate.slatename,
})}`}
/>
</div>
)}
<WebsitePrototypeFooter />
</WebsitePrototypeWrapper>
);
}
}

View File

@ -3,8 +3,7 @@ import * as Utilities from "~/node_common/utilities";
export default async (req, res) => {
const id = Utilities.getIdFromCookie(req);
console.log(id);
console.log(req);
if (!id) {
return res.status(401).send({ decorator: "SERVER_NOT_AUTHENTICATED", error: true });
}

View File

@ -2,6 +2,8 @@ import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import { encryptPasswordClient } from "~/common/utilities";
import JWT from "jsonwebtoken";
@ -19,13 +21,22 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_SIGN_IN_NO_PASSWORD", error: true });
}
const username = req.body.data.username.toLowerCase();
let user;
try {
user = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
} catch (e) {
console.log(e);
if (Validations.email(username)) {
try {
user = await Data.getUserByEmail({ email: username });
} catch (e) {
console.log(e);
}
} else {
try {
user = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
} catch (e) {
console.log(e);
}
}
if (!user) {
@ -36,14 +47,32 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_SIGN_IN_USER_NOT_FOUND", error: true });
}
const hash = await Utilities.encryptPassword(req.body.data.password, user.salt);
if (hash !== user.password) {
return res.status(403).send({ decorator: "SERVER_SIGN_IN_WRONG_PASSWORD", error: true });
// Note(amine): Twitter users won't have a password,
// we should think in the future how to handle this use case
if ((!user.salt || !user.password) && user.twitterId) {
return res.status(403).send({ decorator: "SERVER_TWITTER_LOGIN_ONLY", error: true });
}
await Data.updateUserById({ id: user.id, lastActive: new Date() });
const hash = await Utilities.encryptPassword(req.body.data.password, user.salt);
if (hash !== user.password) {
return res.status(403).send({ decorator: "SERVER_SIGN_IN_WRONG_CREDENTIALS", error: true });
}
let userUpdates = {};
if (user.authVersion === 1) {
const newHash = await encryptPasswordClient(req.body.data.password);
const doubledHash = await Utilities.encryptPassword(newHash, user.salt);
userUpdates = { id: user.id, lastActive: new Date(), authVersion: 2, password: doubledHash };
}
await Data.updateUserById({ id: user.id, lastActive: new Date(), ...userUpdates });
if (!user.email) {
return res
.status(200)
.send({ decorator: "SERVER_SIGN_IN_SHOULD_MIGRATE", shouldMigrate: true });
}
const authorization = Utilities.parseAuthHeader(req.headers.authorization);
if (authorization && !Strings.isEmpty(authorization.value)) {
const verfied = JWT.verify(authorization.value, Environment.JWT_SECRET);

View File

@ -0,0 +1,119 @@
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import JWT from "jsonwebtoken";
import { createOAuthProvider } from "~/node_common/managers/twitter";
const COOKIE_NAME = "oauth_token";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.authToken)) {
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_NO_OAUTH_TOKEN", error: true });
}
if (Strings.isEmpty(req.body.data.authVerifier)) {
return res
.status(500)
.send({ decorator: "SERVER_TWITTER_OAUTH_NO_OAUTH_VERIFIER", error: true });
}
const { authToken, authVerifier } = req.body.data;
const storedAuthToken = req.cookies[COOKIE_NAME];
// NOTE(amine): additional security check
if (authToken !== storedAuthToken) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_INVALID_TOKEN", error: true });
}
let twitterUser;
try {
const { tokenSecret: authSecretToken } = await Data.getTwitterToken({ token: authToken });
const { getOAuthAccessToken, getProtectedResource } = createOAuthProvider();
const { authAccessToken, authSecretAccessToken } = await getOAuthAccessToken({
authToken,
authSecretToken,
authVerifier,
});
const response = await getProtectedResource({
url: "https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
method: "GET",
authAccessToken,
authSecretAccessToken,
});
twitterUser = JSON.parse(response.data);
} catch (err) {
console.log(err);
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_FAILED", error: true });
}
if (!twitterUser) {
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_FAILED", error: true });
}
// const userFriends = await getProtectedResource({
// url: "https://api.twitter.com/1.1/friends/ids.json",
// method: "GET",
// authAccessToken,
// authSecretAccessToken,
// });
// NOTE(Amine): If a user with TwitterId exists
const user = await Data.getUserByTwitterId({ twitterId: twitterUser.id_str });
if (user) {
const token = JWT.sign({ id: user.id, username: user.username }, Environment.JWT_SECRET);
return res.status(200).send({ decorator: "SERVER_SIGN_IN", success: true, token });
}
// NOTE(amine): Twitter account doesn't have an email
if (Strings.isEmpty(twitterUser.email)) {
await Data.updateTwitterToken({
token: authToken,
screen_name: twitterUser.screen_name,
id: twitterUser.id_str,
verified: twitterUser.verified,
});
return res.status(201).send({ decorator: "SERVER_TWITTER_OAUTH_NO_EMAIL" });
}
// NOTE(amine): If there is an account with the user's twitter email
// but not linked to any twitter account
const userByEmail = await Data.getUserByEmail({ email: twitterUser.email });
if (userByEmail && !userByEmail.twitterId) {
await Data.updateUserById({
id: userByEmail.id,
twitterId: twitterUser.id_str,
data: {
...userByEmail.data,
twitter: {
username: twitterUser.screen_name,
verified: twitterUser.verified,
},
},
});
const token = JWT.sign(
{ id: userByEmail.id, username: userByEmail.username },
Environment.JWT_SECRET
);
return res.status(200).send({ decorator: "SERVER_SIGN_IN", success: true, token });
}
//NOTE(amine): If we have twitter email but no user is associated with it
await Data.updateTwitterToken({
token: authToken,
screen_name: twitterUser.screen_name,
email: twitterUser.email,
id: twitterUser.id_str,
verified: twitterUser.verified,
});
return res
.status(200)
.json({ decorator: "SERVER_TWITTER_CONTINUE_SIGNUP", email: twitterUser.email });
};

View File

@ -0,0 +1,29 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import { createOAuthProvider } from "~/node_common/managers/twitter";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_NOT_ALLOWED", error: true });
}
try {
const { getOAuthRequestToken } = createOAuthProvider();
const { authToken, authSecretToken } = await getOAuthRequestToken();
// NOTE(amine): additional security check
res.cookie("oauth_token", authToken, {
maxAge: 15 * 60 * 1000, // 15 minutes
secure: true,
httpOnly: true,
sameSite: true,
});
await Data.createTwitterToken({ token: authToken, tokenSecret: authSecretToken });
res.json({ authToken });
} catch (e) {
console.log("error", e);
res.status(500).send({ decorator: "SERVER_TWITTER_REQUEST_TOKEN_FAILED", error: true });
}
};

View File

@ -0,0 +1,143 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as SlateManager from "~/node_common/managers/slate";
import * as Constants from "~/node_common/constants";
import JWT from "jsonwebtoken";
import { PrivateKey } from "@textile/hub";
export default async (req, res) => {
const { pin, username } = req.body.data;
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_NO_OAUTH_TOKEN", error: true });
}
if (!Validations.verificationPin(pin)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
if (!Validations.username(username)) {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_USERNAME", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.type !== "email_twitter_verification") {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.pin !== req.body.data.pin) {
return res
.status(401)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
const twitterUser = await Data.getTwitterToken({ token: verification.twitterToken });
if (!twitterUser) {
return res.status(401).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
const userByTwitterId = await Data.getUserByTwitterId({ twitterId: twitterUser.id_str });
// NOTE(Amine): If a user with TwitterId exists
if (userByTwitterId) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_TWITTER_EXISTS" });
}
const newUsername = username.toLowerCase();
const newEmail = verification.email.toLowerCase();
// NOTE(Amine): If there is an account with the user's twitter email
const userByEmail = await Data.getUserByEmail({ email: newEmail });
if (userByEmail) return res.status(201).send({ decorator: "SERVER_CREATE_USER_EMAIL_TAKEN" });
// NOTE(Amine): If there is an account with the provided username
const userByUsername = await Data.getUserByUsername({ username });
if (userByUsername) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN" });
}
// TODO(jim):
// Single Key Textile Auth.
const identity = await PrivateKey.fromRandom();
const api = identity.toString();
// TODO(jim):
// Don't do this once you refactor.
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken({
user: {
username: newUsername,
data: { tokens: { api } },
},
});
if (!buckets) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_USER_BUCKET_INIT_FAILURE", error: true });
}
const photo = await SlateManager.getRandomSlateElementURL({
id: Environment.AVATAR_SLATE_ID,
fallback:
"https://slate.textile.io/ipfs/bafkreick3nscgixwfpq736forz7kzxvvhuej6kszevpsgmcubyhsx2pf7i",
});
const user = await Data.createUser({
username: newUsername,
email: newEmail,
twitterId: twitterUser.id_str,
data: {
photo,
body: "",
settings: {
settings_deals_auto_approve: false,
allow_filecoin_directory_listing: false,
allow_automatic_data_storage: true,
allow_encrypted_data_storage: true,
},
tokens: { api },
twitter: {
username: twitterUser.screen_name,
verified: twitterUser.verified,
},
},
});
if (!user) {
return res.status(404).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
if (user.error) {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
const token = JWT.sign({ id: user.id, username: user.username }, Environment.JWT_SECRET);
return res.status(200).send({ decorator: "SERVER_SIGN_IN", success: true, token });
};

125
pages/api/twitter/signup.js Normal file
View File

@ -0,0 +1,125 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as SlateManager from "~/node_common/managers/slate";
import JWT from "jsonwebtoken";
import { PrivateKey } from "@textile/hub";
const COOKIE_NAME = "oauth_token";
export default async (req, res) => {
const { authToken, email, username } = req.body.data;
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(authToken)) {
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_NO_OAUTH_TOKEN", error: true });
}
if (!Validations.email(email)) {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_EMAIL", error: true });
}
if (!Validations.username(username)) {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_USERNAME", error: true });
}
const storedAuthToken = req.cookies[COOKIE_NAME];
// NOTE(amine): additional security check
if (authToken !== storedAuthToken) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
const twitterUser = await Data.getTwitterToken({ token: authToken });
if (!twitterUser) {
return res.status(401).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
if (twitterUser.email !== email) {
return res.status(401).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
const userByTwitterId = await Data.getUserByTwitterId({ twitterId: twitterUser.id_str });
// NOTE(Amine): If a user with TwitterId exists
if (userByTwitterId) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_TWITTER_EXISTS" });
}
// NOTE(Amine): If there is an account with the user's twitter email
const userByEmail = await Data.getUserByEmail({ email: twitterUser.email });
if (userByEmail) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_EMAIL_TAKEN" });
}
// NOTE(Amine): If there is an account with the provided username
const userByUsername = await Data.getUserByUsername({ username });
if (userByUsername) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN" });
}
// TODO(jim):
// Single Key Textile Auth.
const identity = await PrivateKey.fromRandom();
const api = identity.toString();
const newUsername = username.toLowerCase();
const newEmail = email.toLowerCase();
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken({
user: {
username: newUsername,
data: { tokens: { api } },
},
});
if (!buckets) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_USER_BUCKET_INIT_FAILURE", error: true });
}
const photo = await SlateManager.getRandomSlateElementURL({
id: Environment.AVATAR_SLATE_ID,
fallback:
"https://slate.textile.io/ipfs/bafkreick3nscgixwfpq736forz7kzxvvhuej6kszevpsgmcubyhsx2pf7i",
});
const user = await Data.createUser({
username: newUsername,
email: newEmail,
twitterId: twitterUser.id_str,
data: {
photo,
body: "",
settings: {
settings_deals_auto_approve: false,
allow_filecoin_directory_listing: false,
allow_automatic_data_storage: true,
allow_encrypted_data_storage: true,
},
tokens: { api },
twitter: {
username: twitterUser.screen_name,
verified: twitterUser.verified,
},
},
});
if (!user) {
return res.status(404).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
if (user.error) {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
const token = JWT.sign({ id: user.id, username: user.username }, Environment.JWT_SECRET);
return res.status(200).send({ decorator: "SERVER_SIGN_IN", success: true, token });
};

View File

@ -0,0 +1,20 @@
import * as Data from "~/node_common/data";
export default async (req, res) => {
const userByEmail = await Data.getUserByEmail({
email: req.body.data.email.toLowerCase(),
});
if (!userByEmail) {
return res.status(200).send({ decorator: "SERVER_USER_NOT_FOUND" });
}
if (userByEmail.error) {
return res.status(500).send({ decorator: "SERVER_USER_NOT_FOUND", error: true });
}
return res.status(200).send({
decorator: "SERVER_CHECK_EMAIL",
data: { email: !!userByEmail.email, twitter: !!!userByEmail.password },
});
};

View File

@ -4,6 +4,7 @@ import * as Utilities from "~/node_common/utilities";
import * as SlateManager from "~/node_common/managers/slate";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as EmailManager from "~/node_common/managers/emails";
import BCrypt from "bcrypt";
@ -14,18 +15,6 @@ export default async (req, res) => {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.accepted)) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_ACCEPT_TERMS", error: true });
}
const existing = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
if (existing) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN", error: true });
}
if (!Validations.username(req.body.data.username)) {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_USERNAME", error: true });
}
@ -34,6 +23,32 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_PASSWORD", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_TOKEN", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
if (!verification.isVerified) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_EMAIL_UNVERIFIED", error: true });
}
const existing = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
if (existing) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN", error: true });
}
const existingViaEmail = await Data.getUserByEmail({ email: verification.email });
if (existingViaEmail) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_EMAIL_TAKEN", error: true });
}
const rounds = Number(Environment.LOCAL_PASSWORD_ROUNDS);
const salt = await BCrypt.genSalt(rounds);
const hash = await Utilities.encryptPassword(req.body.data.password, salt);
@ -46,6 +61,7 @@ export default async (req, res) => {
// TODO(jim):
// Don't do this once you refactor.
const newUsername = req.body.data.username.toLowerCase();
const newEmail = verification.email;
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken({
user: {
@ -70,6 +86,7 @@ export default async (req, res) => {
password: hash,
salt,
username: newUsername,
email: newEmail,
data: {
photo,
body: "",
@ -91,8 +108,20 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_FAILED", error: true });
}
return res.status(200).send({
// Note(amine): we can respond early to the client, sending the welcome email isn't a necessary part
res.status(200).send({
decorator: "SERVER_CREATE_USER",
user: { username: user.username, id: user.id },
});
const welcomeTemplateId = "d-7688a09484194c06a417a434eaaadd6e";
const slateEmail = "hello@slate.host";
await EmailManager.sendTemplate({
to: user.email,
from: slateEmail,
templateId: welcomeTemplateId,
});
// Monitor.createUser({ user });
};

View File

@ -0,0 +1,40 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_NOT_ALLOWED", error: true });
}
const username = req.body.data.username.toLowerCase();
let user;
if (Validations.email(username)) {
try {
user = await Data.getUserByEmail({ email: username });
} catch (e) {
console.log(e);
}
} else {
try {
user = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
} catch (e) {
console.log(e);
}
}
if (!user) {
return res.status(200).send({ decorator: "SERVER_USER_NOT_FOUND" });
}
if (user.error) {
return res.status(500).send({ decorator: "SERVER_USER_NOT_FOUND", error: true });
}
return res
.status(200)
.send({ decorator: "SERVER_GET_USER_VERSION", data: { version: user.authVersion } });
};

View File

@ -0,0 +1,67 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as SlateManager from "~/node_common/managers/slate";
import * as Constants from "~/node_common/constants";
import JWT from "jsonwebtoken";
import { PrivateKey } from "@textile/hub";
import { Verification } from "~/components/core/Auth/components";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_MIGRATE_USER_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res.status(500).send({ decorator: "SERVER_MIGRATE_USER_NO_TOKEN", error: true });
}
if (!Validations.verificationPin(req.body.data.pin)) {
return res.status(500).send({ decorator: "SERVER_MIGRATE_USER_INVALID_PIN", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.type !== "user_migration") {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.pin !== req.body.data.pin) {
return res.status(401).send({ decorator: "SERVER_MIGRATE_USER_INVALID_PIN", error: true });
}
const username = verification.username;
const email = verification.email;
const user = await Data.getUserByUsername({ username });
if (!user) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
if (user.error) {
return res.status(500).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
await Data.updateUserById({ id: user.id, email });
return res.status(200).send({ decorator: "SERVER_MIGRATE_USER", success: true });
};

View File

@ -0,0 +1,75 @@
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as Constants from "~/node_common/constants";
import BCrypt from "bcrypt";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_RESET_PASSWORD_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res.status(500).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
if (!Validations.password(req.body.data.password)) {
return res.status(500).send({ decorator: "SERVER_RESET_PASSWORD_NO_PASSWORD", error: true });
}
const token = req.body.data.token;
const verification = await Data.getVerificationBySid({
sid: token,
});
if (!verification) {
return res.status(401).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
if (verification.error) {
return res.status(401).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
if (
verification.type !== "password_reset" ||
!verification.isVerified ||
verification.passwordChanged
) {
return res.status(401).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
const email = verification.email;
const user = await Data.getUserByEmail({ email });
if (!user) {
return res.status(404).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
if (user.error) {
return res.status(500).send({ decorator: "SERVER_RESET_PASSWORD_FAILED", error: true });
}
const rounds = Number(Environment.LOCAL_PASSWORD_ROUNDS);
const salt = await BCrypt.genSalt(rounds);
const hash = await Utilities.encryptPassword(req.body.data.password, salt);
await Data.updateUserById({
id: user.id,
salt,
password: hash,
authVersion: 2,
lastActive: new Date(),
});
await Data.updateVerification({ sid: token, passwordChanged: true });
res.status(200).send({ decorator: "SERVER_PASSWORD_RESET", success: true });
};

View File

@ -48,6 +48,23 @@ export default async (req, res) => {
}
}
if (updates.email && updates.email !== user.email) {
if (!Validations.email(req.body.data.email)) {
return res.status(400).send({
decorator: "SERVER_USER_UPDATE_INVALID_EMAIL",
error: true,
});
}
const existing = await Data.getUserByEmail({
email: req.body.data.email.toLowerCase(),
});
if (existing && existing.id !== id) {
return res.status(500).send({ decorator: "SERVER_USER_UPDATE_EMAIL", error: true });
}
}
if (req.body.data.type === "SAVE_DEFAULT_ARCHIVE_CONFIG") {
let b;
try {

View File

@ -0,0 +1,61 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as EmailManager from "~/node_common/managers/emails";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_CREATE_VERIFICATION_NOT_ALLOWED", error: true });
}
if (!Validations.email(req.body?.data?.email)) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_VERIFICATION_INVALID_EMAIL", error: true });
}
const email = req.body.data.email.toLowerCase();
const userByEmail = await Data.getUserByEmail({ email });
if (userByEmail) {
return res
.status(409)
.send({ decorator: "SERVER_CREATE_VERIFICATION_EMAIL_TAKEN", error: true });
}
const pin = Utilities.generateRandomNumberInRange(111111, 999999);
const verification = await Data.createVerification({
email,
pin,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
const confTemplateId = "d-823d8ae5e838452f903e94ee4115bffc";
const slateEmail = "hello@slate.host";
const sentEmail = await EmailManager.sendTemplate({
to: email,
from: slateEmail,
templateId: confTemplateId,
templateData: { confirmation_code: pin },
});
if (sentEmail?.error) {
return res.status(500).send({ decorator: sentEmail.decorator, error: true });
}
return res.status(200).send({
decorator: "SERVER_CREATE_VERIFICATION",
token: verification.sid,
});
};

View File

@ -0,0 +1,98 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as EmailManager from "~/node_common/managers/emails";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_CREATE_VERIFICATION_NOT_ALLOWED", error: true });
}
if (!Validations.email(req.body?.data?.email)) {
return res
.status(400)
.send({ decorator: "SERVER_CREATE_VERIFICATION_INVALID_EMAIL", error: true });
}
if (!Validations.username(req.body?.data?.username)) {
return res
.status(400)
.send({ decorator: "SERVER_CREATE_VERIFICATION_INVALID_USERNAME", error: true });
}
if (!Validations.password(req.body?.data?.password)) {
return res
.status(400)
.send({ decorator: "SERVER_CREATE_VERIFICATION_INVALID_PASSWORD", error: true });
}
const email = req.body.data.email.toLowerCase();
const userByEmail = await Data.getUserByEmail({ email });
if (userByEmail) {
return res
.status(409)
.send({ decorator: "SERVER_CREATE_VERIFICATION_EMAIL_TAKEN", error: true });
}
const username = req.body.data.username.toLowerCase();
const user = await Data.getUserByUsername({ username });
if (!user) {
return res
.status(404)
.send({ decorator: "SERVER_CREATE_VERIFICATION_USER_NOT_FOUND", error: true });
}
if (user.error) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_VERIFICATION_USER_NOT_FOUND", error: true });
}
const password = req.body.data.password;
const hash = await Utilities.encryptPassword(password, user.salt);
if (hash !== user.password) {
return res
.status(403)
.send({ decorator: "SERVER_CREATE_VERIFICATION_WRONG_PASSWORD", error: true });
}
const pin = Utilities.generateRandomNumberInRange(111111, 999999);
const verification = await Data.createVerification({
email,
pin,
username,
type: "user_migration",
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
const confTemplateId = "d-823d8ae5e838452f903e94ee4115bffc";
const slateEmail = "hello@slate.host";
const sentEmail = await EmailManager.sendTemplate({
to: email,
from: slateEmail,
templateId: confTemplateId,
templateData: { confirmation_code: pin },
});
if (sentEmail?.error) {
return res.status(500).send({ decorator: sentEmail.decorator, error: true });
}
return res.status(200).send({
decorator: "SERVER_CREATE_VERIFICATION",
token: verification.sid,
});
};

View File

@ -0,0 +1,70 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as EmailManager from "~/node_common/managers/emails";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_CREATE_VERIFICATION_NOT_ALLOWED", error: true });
}
if (!Validations.email(req.body?.data?.email)) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_VERIFICATION_INVALID_EMAIL", error: true });
}
const email = req.body.data.email.toLowerCase();
const user = await Data.getUserByEmail({ email });
if (!user) {
return res
.status(404)
.send({ decorator: "SERVER_CREATE_VERIFICATION_USER_NOT_FOUND", error: true });
}
if (user.error) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_VERIFICATION_USER_NOT_FOUND", error: true });
}
const pin = Utilities.generateRandomNumberInRange(111111, 999999);
const verification = await Data.createVerification({
email,
pin,
type: "password_reset",
passwordChanged: false,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
const confTemplateId = "d-823d8ae5e838452f903e94ee4115bffc";
const slateEmail = "hello@slate.host";
const sentEmail = await EmailManager.sendTemplate({
to: email,
from: slateEmail,
templateId: confTemplateId,
templateData: { confirmation_code: pin },
});
if (sentEmail?.error) {
return res.status(500).send({ decorator: sentEmail.decorator, error: true });
}
return res.status(200).send({
decorator: "SERVER_CREATE_VERIFICATION",
token: verification.sid,
});
};

View File

@ -0,0 +1,58 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Constants from "~/node_common/constants";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res.status(500).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
if (!Validations.verificationPin(req.body.data.pin)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
if (!verification) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.type !== "password_reset") {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.pin !== req.body.data.pin) {
return res
.status(401)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
await Data.updateVerification({ sid: req.body.data.token, isVerified: true });
return res.status(200).send({
decorator: "SERVER_EMAIL_VERIFICATION_SUCCESS",
token: verification,
});
};

View File

@ -0,0 +1,20 @@
import * as Data from "~/node_common/data";
export default async (req, res) => {
//NOTE(toast): restrict pruning old verifications to backend to prevent DoS
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_PRUNE_VERIFICATION_NOT_ALLOWED", error: true });
}
prune = await Data.pruneVerifications();
if (prune.error || !prune) {
return res.status(404).send({
decorator: "SERVER_PRUNE_VERIFICATIONS_FAILED",
});
}
return res.status(200).send({
decorator: "SERVER_PRUNE_VERIFICATIONS",
});
};

View File

@ -0,0 +1,55 @@
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as EmailManager from "~/node_common/managers/emails";
import * as Constants from "~/node_common/constants";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_TOKEN", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const confTemplateId = "d-823d8ae5e838452f903e94ee4115bffc";
const slateEmail = "hello@slate.host";
const sentEmail = await EmailManager.sendTemplate({
to: verification.email,
from: slateEmail,
templateId: confTemplateId,
templateData: { confirmation_code: verification.pin },
});
if (sentEmail?.error) {
return res.status(500).send({ decorator: sentEmail.decorator, error: true });
}
return res.status(200).send({
decorator: "SERVER_EMAIL_VERIFICATION_RESEND_SUCCESS",
});
};

View File

@ -0,0 +1,76 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as EmailManager from "~/node_common/managers/emails";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
try {
if (
!Strings.isEmpty(Environment.ALLOWED_HOST) &&
req.headers.host !== Environment.ALLOWED_HOST
) {
return res
.status(403)
.send({ decorator: "SERVER_CREATE_VERIFICATION_NOT_ALLOWED", error: true });
}
if (!Validations.email(req.body.data.email)) {
return res
.status(500)
.send({ decorator: "SERVER_CREATE_VERIFICATION_INVALID_EMAIL", error: true });
}
if (Strings.isEmpty(req.body.data.twitterToken)) {
return res.status(500).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const email = req.body.data.email.toLowerCase();
const userByEmail = await Data.getUserByEmail({ email });
if (userByEmail) {
return res
.status(409)
.send({ decorator: "SERVER_CREATE_VERIFICATION_EMAIL_TAKEN", error: true });
}
const twitterToken = req.body.data.twitterToken;
const pin = Utilities.generateRandomNumberInRange(111111, 999999);
const verification = await Data.createVerification({
email,
pin,
type: "email_twitter_verification",
twitterToken,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_CREATE_VERIFICATION_FAILED", error: true });
}
const confTemplateId = "d-823d8ae5e838452f903e94ee4115bffc";
const slateEmail = "hello@slate.host";
const sentEmail = await EmailManager.sendTemplate({
to: email,
from: slateEmail,
templateId: confTemplateId,
templateData: { confirmation_code: pin },
});
if (sentEmail?.error) {
return res.status(500).send({ decorator: sentEmail.decorator, error: true });
}
return res.status(200).send({
decorator: "SERVER_CREATE_VERIFICATION",
token: verification.sid,
});
} catch (e) {
console.log(e);
}
};

View File

@ -0,0 +1,63 @@
import * as Data from "~/node_common/data";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Constants from "~/node_common/constants";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return (
res
.status(500)
// NOTE(amine): I chose this decorator because token is used internally
.send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true })
);
}
if (!Validations.verificationPin(req.body.data.pin)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
if (!verification) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.type !== "email_verification") {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.pin !== req.body.data.pin) {
return res
.status(401)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
await Data.updateVerification({ sid: req.body.data.token, isVerified: true });
return res.status(200).send({
decorator: "SERVER_EMAIL_VERIFICATION_SUCCESS",
token: verification,
});
};

View File

@ -7,7 +7,7 @@ import { css } from "@emotion/react";
import Prism from "prismjs";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeHeader from "~/components/core/NewWebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/NewWebsitePrototypeFooter";
const SLATE_CORE_TEAM = [

View File

@ -5,7 +5,7 @@ import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeHeader from "~/components/core/NewWebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/NewWebsitePrototypeFooter";
const STYLES_ROOT = css`

View File

@ -2,7 +2,7 @@ import * as React from "react";
import * as Constants from "~/common/constants";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeHeader from "~/components/core/NewWebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/NewWebsitePrototypeFooter";
import { css } from "@emotion/react";
@ -370,8 +370,8 @@ export default class IndexPage extends React.Component {
organize, and link files together.
</p>
<br />
<a style={{ textDecoration: `none` }} href="/_/auth">
<ButtonPrimary>Get started with Slate</ButtonPrimary>
<a style={{ textDecoration: `none` }} href="/_">
<ButtonPrimary>Try it out</ButtonPrimary>
</a>
</div>
<video

View File

@ -2,7 +2,7 @@ import * as React from "react";
import * as Constants from "~/common/constants";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeHeader from "~/components/core/NewWebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/NewWebsitePrototypeFooter";
import { css } from "@emotion/react";

View File

@ -5,7 +5,7 @@ import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import WebsitePrototypeHeader from "~/components/core/WebsitePrototypeHeader";
import WebsitePrototypeHeader from "~/components/core/NewWebsitePrototypeHeader";
import WebsitePrototypeFooter from "~/components/core/NewWebsitePrototypeFooter";
const STYLES_ROOT = css`

View File

@ -216,17 +216,6 @@ export default class SceneActivity extends React.Component {
window.removeEventListener("scroll", this.scrollDebounceInstance);
}
getTab = () => {
const tab = this.props.page.params?.tab;
if (tab) {
return tab;
}
if (this.props.viewer?.following?.length || this.props.viewer?.subscriptions?.length) {
return "activity";
}
return "explore";
};
_handleScroll = (e) => {
if (this.state.loading) {
return;
@ -250,7 +239,14 @@ export default class SceneActivity extends React.Component {
fetchActivityItems = async (update = false) => {
if (this.state.loading === "loading") return;
let tab = this.getTab();
let tab = this.props.page.params?.tab || "explore";
// if (!tab) {
// if (this.props.viewer) {
// tab = "activity";
// } else {
// tab = "explore";
// }
// }
const isExplore = tab === "explore";
this.setState({ loading: "loading" });
let activity;
@ -363,8 +359,16 @@ export default class SceneActivity extends React.Component {
};
render() {
let tab = this.getTab();
let tab = this.props.page.params?.tab || "explore";
// if (!tab) {
// if (this.props.viewer) {
// tab = "activity";
// } else {
// tab = "explore";
// }
// }
let activity;
if (this.props.viewer) {
activity =
tab === "activity" ? this.props.viewer?.activity || [] : this.props.viewer?.explore || [];

Some files were not shown because too many files have changed in this diff Show More