mirror of
https://github.com/filecoin-project/slate.git
synced 2025-01-02 22:27:00 +03:00
squashing commits
This commit is contained in:
parent
91ea4d9d9c
commit
e8e1e1f26e
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
@ -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 }),
|
||||
});
|
||||
|
@ -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",
|
||||
};
|
||||
|
||||
|
@ -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
187
common/hooks.js
Normal 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 };
|
||||
};
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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) {
|
||||
|
@ -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 [];
|
||||
|
@ -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 = "") => {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
227
components/core/Auth/Initial.js
Normal file
227
components/core/Auth/Initial.js
Normal 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>
|
||||
);
|
||||
}
|
183
components/core/Auth/ResetPassword.js
Normal file
183
components/core/Auth/ResetPassword.js
Normal 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>
|
||||
);
|
||||
}
|
229
components/core/Auth/Signin.js
Normal file
229
components/core/Auth/Signin.js
Normal 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>
|
||||
);
|
||||
}
|
100
components/core/Auth/Signup.js
Normal file
100
components/core/Auth/Signup.js
Normal 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>
|
||||
);
|
||||
}
|
122
components/core/Auth/TwitterSignup.js
Normal file
122
components/core/Auth/TwitterSignup.js
Normal 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>
|
||||
);
|
||||
}
|
64
components/core/Auth/components/AuthCheckBox.js
Normal file
64
components/core/Auth/components/AuthCheckBox.js
Normal 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>
|
||||
);
|
||||
}
|
74
components/core/Auth/components/SignUpPopover.js
Normal file
74
components/core/Auth/components/SignUpPopover.js
Normal 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>
|
||||
);
|
||||
}
|
75
components/core/Auth/components/Toggle.js
Normal file
75
components/core/Auth/components/Toggle.js
Normal 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>
|
||||
);
|
||||
}
|
126
components/core/Auth/components/Verification.js
Normal file
126
components/core/Auth/components/Verification.js
Normal 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}>
|
||||
Didn’t receive an email? <ResendButton onResend={onResend} />
|
||||
</System.P>
|
||||
</motion.div>
|
||||
</AnimateSharedLayout>
|
||||
</SignUpPopover>
|
||||
);
|
||||
}
|
4
components/core/Auth/components/index.js
Normal file
4
components/core/Auth/components/index.js
Normal 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";
|
5
components/core/Auth/index.js
Normal file
5
components/core/Auth/index.js
Normal 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
140
components/core/Field.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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`
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
240
components/core/NewWebsitePrototypeHeader.js
Normal file
240
components/core/NewWebsitePrototypeHeader.js
Normal 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;
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
104
components/core/WebsitePrototypeHeaderGeneric.js
Normal file
104
components/core/WebsitePrototypeHeaderGeneric.js
Normal 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;
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
15
components/system/components/Divider.js
Normal file
15
components/system/components/Divider.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
};
|
||||
|
24
node_common/data/methods/create-twitter-token.js
Normal file
24
node_common/data/methods/create-twitter-token.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
@ -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("*");
|
||||
|
29
node_common/data/methods/create-verification.js
Normal file
29
node_common/data/methods/create-verification.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
18
node_common/data/methods/delete-verification-by-email.js
Normal file
18
node_common/data/methods/delete-verification-by-email.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
18
node_common/data/methods/delete-verification-by-sid.js
Normal file
18
node_common/data/methods/delete-verification-by-sid.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
26
node_common/data/methods/get-twitter-token.js
Normal file
26
node_common/data/methods/get-twitter-token.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
29
node_common/data/methods/get-user-by-email.js
Normal file
29
node_common/data/methods/get-user-by-email.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
27
node_common/data/methods/get-user-by-twitter-id.js
Normal file
27
node_common/data/methods/get-user-by-twitter-id.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
26
node_common/data/methods/get-verification-by-email.js
Normal file
26
node_common/data/methods/get-verification-by-email.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
25
node_common/data/methods/get-verification-by-sid.js
Normal file
25
node_common/data/methods/get-verification-by-sid.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
21
node_common/data/methods/prune-verifications.js
Normal file
21
node_common/data/methods/prune-verifications.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
27
node_common/data/methods/update-twitter-token.js
Normal file
27
node_common/data/methods/update-twitter-token.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
@ -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) => {
|
||||
|
25
node_common/data/methods/update-verification.js
Normal file
25
node_common/data/methods/update-verification.js
Normal 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",
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
@ -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;
|
||||
|
72
node_common/managers/emails.js
Normal file
72
node_common/managers/emails.js
Normal 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 };
|
||||
}
|
||||
};
|
73
node_common/managers/twitter.js
Normal file
73
node_common/managers/twitter.js
Normal 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),
|
||||
};
|
||||
};
|
@ -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,
|
||||
|
@ -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
4326
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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
102
pages/_/profile[dep].js
Normal 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
375
pages/_/slate[dep].js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
|
@ -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);
|
||||
|
119
pages/api/twitter/authenticate.js
Normal file
119
pages/api/twitter/authenticate.js
Normal 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 });
|
||||
};
|
29
pages/api/twitter/request-token.js
Normal file
29
pages/api/twitter/request-token.js
Normal 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 });
|
||||
}
|
||||
};
|
143
pages/api/twitter/signup-with-verification.js
Normal file
143
pages/api/twitter/signup-with-verification.js
Normal 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
125
pages/api/twitter/signup.js
Normal 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 });
|
||||
};
|
20
pages/api/users/check-email.js
Normal file
20
pages/api/users/check-email.js
Normal 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 },
|
||||
});
|
||||
};
|
@ -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 });
|
||||
};
|
||||
|
40
pages/api/users/get-version.js
Normal file
40
pages/api/users/get-version.js
Normal 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 } });
|
||||
};
|
67
pages/api/users/migrate.js
Normal file
67
pages/api/users/migrate.js
Normal 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 });
|
||||
};
|
75
pages/api/users/reset-password.js
Normal file
75
pages/api/users/reset-password.js
Normal 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 });
|
||||
};
|
@ -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 {
|
||||
|
61
pages/api/verifications/create.js
Normal file
61
pages/api/verifications/create.js
Normal 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,
|
||||
});
|
||||
};
|
98
pages/api/verifications/legacy/create.js
Normal file
98
pages/api/verifications/legacy/create.js
Normal 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,
|
||||
});
|
||||
};
|
70
pages/api/verifications/password-reset/create.js
Normal file
70
pages/api/verifications/password-reset/create.js
Normal 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,
|
||||
});
|
||||
};
|
58
pages/api/verifications/password-reset/verify.js
Normal file
58
pages/api/verifications/password-reset/verify.js
Normal 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,
|
||||
});
|
||||
};
|
20
pages/api/verifications/prune.js
Normal file
20
pages/api/verifications/prune.js
Normal 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",
|
||||
});
|
||||
};
|
55
pages/api/verifications/resend.js
Normal file
55
pages/api/verifications/resend.js
Normal 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",
|
||||
});
|
||||
};
|
76
pages/api/verifications/twitter/create.js
Normal file
76
pages/api/verifications/twitter/create.js
Normal 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);
|
||||
}
|
||||
};
|
63
pages/api/verifications/verify.js
Normal file
63
pages/api/verifications/verify.js
Normal 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,
|
||||
});
|
||||
};
|
@ -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 = [
|
||||
|
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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`
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user