diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4014c28..8ef3009d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,19 +55,6 @@ jobs: APP=admin START_COMMAND=start - - name: Build Server - uses: docker/build-push-action@v3 - with: - context: . - push: true - tags: ghcr.io/${{ github.repository }}/server:${{ github.event.inputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - COMMIT=${{ steps.vars.outputs.sha_short }} - APP=server - START_COMMAND=start - - name: Build Frontend uses: docker/build-push-action@v3 with: diff --git a/.gitignore b/.gitignore index c3c9c20b..dcbd94ae 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ dist/ .vscode .backups +.ethereal diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index ba5db7b3..6194c33e 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 2dacc90c..7f15f2ef 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -13,7 +13,6 @@ import { openApiDocument } from './openapi'; const trpcApiEndpoint = '/trpc' - async function main() { // express implementation const app = express(); @@ -48,7 +47,6 @@ async function main() { createContext: createRPCContext, }), ); - // Handle incoming OpenAPI requests app.use('/api', createOpenApiExpressMiddleware({ router: appRouter, createContext: createRPCContext })); diff --git a/apps/frontend/jsconfig.json b/apps/frontend/jsconfig.json index 82413fbb..749280c0 100644 --- a/apps/frontend/jsconfig.json +++ b/apps/frontend/jsconfig.json @@ -40,9 +40,6 @@ ], "~/*": [ "src/*" - ], - "~login/*": [ - "src/components/login/*" ] } } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 38125642..2a09cdd5 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -33,7 +33,6 @@ "@mui/lab": "^5.0.0-alpha.148", "@mui/material": "^5.14.13", "@mui/styles": "^5.14.13", - "@reduxjs/toolkit": "^1.8.6", "@tanstack/react-query": "^4.36.1", "@trpc/client": "^10.40.0", "@trpc/react-query": "^10.40.0", @@ -42,7 +41,6 @@ "autosuggest-highlight": "^3.3.4", "axios": "^1.3.4", "change-case": "^4.1.2", - "connected-react-router": "6.9.3", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.10", "enzyme": "^3.3.0", @@ -61,7 +59,6 @@ "notistack": "^3.0.1", "passport": "^0.6.0", "passport-local": "^1.0.0", - "prop-types": "^15.6.2", "query-string": "^6.1.0", "ramda": "^0.28.0", "randomcolor": "^0.5.3", @@ -73,21 +70,18 @@ "react-error-boundary": "^4.0.11", "react-full-screen": "^0.2.2", "react-i18next": "^13.2.2", - "react-redux": "^8.0.4", "react-router": "^6.17.0", "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", "react-transition-group": "^2.3.1", "react-use-event": "^1.1.1", "recoil": "^0.7.7", - "redux": "^4.0.0", - "redux-devtools-extension": "^2.13.5", - "redux-thunk": "^2.3.0", "rooks": "^7.4.1", "serve": "^14.2.1", "shiitake": "^3.0.2", "typescript": "^5.2.2", - "yup": "^1.3.2" + "yup": "^1.3.2", + "yup-locales": "^1.2.18" }, "browserslist": { "production": [ diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 53c34a23..759d9baa 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -13,20 +13,25 @@ import { ConfirmProvider } from "material-ui-confirm"; import { SnackbarProvider } from "notistack"; import React, { Suspense, useState } from "react"; import { initReactI18next } from "react-i18next"; -import { Provider } from "react-redux"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; import { RecoilRoot } from "recoil"; +import { setLocale } from "yup"; +import { fr } from "yup-locales"; -import { LogintDialog } from "~components/LoginDialog"; +import { ConfirmDialog } from "~components/login/ConfirmDialog"; +import { ForgotDialog } from "~components/login/ForgotDialog"; +import { JoinDialog } from "~components/login/JoinDialog"; +import { LoginDialog } from "~components/login/LoginDialog"; +import { RecoverDialog } from "~components/login/RecoverDialog"; +import { SignupDialog } from "~components/login/SignupDialog"; +import { StudentSignupDialog } from "~components/login/StudentSignupDialog"; import { SharedLayout } from "~components/SharedLayout"; -import { SignupDialog } from "~components/SignupDialog"; import { trpc } from "~utils/trpc"; import ResetScroll from "./components/ResetScroll"; -import UpdateIndicator from "./components/UpdateIndicator"; -// import { ConnectedRouter } from "connected-react-router"; -import en from "./locales/en/common.json"; -import fr from "./locales/fr/common.json"; +import { UpdateIndicator } from "./components/UpdateIndicator"; +import commonEN from "./locales/en/common.json"; +import commonFR from "./locales/fr/common.json"; import { About } from "./pages/about"; import { CreateProjectPage } from "./pages/create"; import { HomePage } from "./pages/home"; @@ -35,7 +40,6 @@ import UserProfile from "./pages/profile"; import ProjectPage from "./pages/project"; import { SharePage } from "./pages/share"; import { TermsAndConditions } from "./pages/terms"; -import createAppStore from "./store"; import { createTheme } from "./theme"; dayjs.extend(relativeTime); @@ -43,6 +47,8 @@ dayjs.extend(isLeapYear); // use plugin dayjs.extend(duration); dayjs.locale("fr-fr"); // use locale +setLocale(fr); + i18next .use(LanguageDetector) .use(initReactI18next) @@ -50,10 +56,10 @@ i18next debug: false, resources: { en_US: { - translations: en, + translations: commonEN, }, fr_FR: { - translations: fr, + translations: commonFR, }, }, ns: ["translations"], @@ -64,8 +70,6 @@ i18next }, } as i18next.InitOptions); -const store = createAppStore(); - const AppRouters = () => { const location = useLocation(); const { state } = location; @@ -88,8 +92,13 @@ const AppRouters = () => { {state?.backgroundLocation && ( - } /> + } /> + } /> + } /> + } /> } /> + } /> + } /> )} @@ -116,28 +125,26 @@ const App = () => { return ( - - - - - - - + + + + + + + - - - - - - - + + + + + - - - - - - + + + + + + ); diff --git a/apps/frontend/src/actions/AppActions.tsx b/apps/frontend/src/actions/AppActions.tsx deleted file mode 100644 index 7673f4b5..00000000 --- a/apps/frontend/src/actions/AppActions.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { ActionType, createEmptyAction } from "~types/ActionTypes"; - -export const applicationUpdated = () => - createEmptyAction(ActionType.APPLICATION_UPDATED); diff --git a/apps/frontend/src/actions/Signin/LoginActions.tsx b/apps/frontend/src/actions/Signin/LoginActions.tsx deleted file mode 100644 index 3b6230c6..00000000 --- a/apps/frontend/src/actions/Signin/LoginActions.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Credentials, SigninErrors, SigninResult } from "@celluloid/types"; -import { Dispatch } from "redux"; - -import UserService from "~services/UserService"; -import { Action, ActionType, createEmptyAction } from "~types/ActionTypes"; - -import { triggerSigninLoading } from "."; -import { openConfirmSignup } from "./SignupActions"; -import { fetchCurrentUserThunk } from "./UserActions"; - -export const openLogin = () => createEmptyAction(ActionType.OPEN_LOGIN); - -export const succeedLogin = () => createEmptyAction(ActionType.SUCCEED_LOGIN); - -export function failLogin(errors: SigninErrors): Action { - return { - type: ActionType.FAIL_LOGIN, - payload: errors, - error: true, - }; -} - -export const doLoginThunk = - (credentials: Credentials) => (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.login(credentials) - .then((result: SigninResult) => { - if (!result.success) { - if (result.errors.server === "UserNotConfirmed") { - return dispatch(openConfirmSignup(credentials)); - } else { - return dispatch(failLogin(result.errors)); - } - } else { - fetchCurrentUserThunk()(dispatch); - return dispatch(succeedLogin()); - } - }) - .catch(() => { - return dispatch(failLogin({ server: "RequestFailed" })); - }); - }; diff --git a/apps/frontend/src/actions/Signin/ResetPasswordActions.tsx b/apps/frontend/src/actions/Signin/ResetPasswordActions.tsx deleted file mode 100644 index 36982543..00000000 --- a/apps/frontend/src/actions/Signin/ResetPasswordActions.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - SigninErrors, - SigninResult, - TeacherConfirmResetPasswordData, -} from "@celluloid/types"; -import { Dispatch } from "redux"; - -import UserService from "~services/UserService"; -import { - ActionType, - createAction, - createEmptyAction, - createErrorAction, -} from "~types/ActionTypes"; - -import { doLoginThunk, triggerSigninLoading } from "."; - -export const openResetPassword = () => - createEmptyAction(ActionType.OPEN_RESET_PASSWORD); - -export const succeedResetPassword = () => - createEmptyAction(ActionType.SUCCEED_RESET_PASSWORD); - -export const openConfirmResetPassword = (email: string) => - createAction(ActionType.OPEN_CONFIRM_RESET_PASSWORD, email); - -export const failResetPassword = (errors: SigninErrors) => - createErrorAction(ActionType.FAIL_RESET_PASSWORD, errors); - -export const failConfirmResetPassword = (errors: SigninErrors) => - createErrorAction(ActionType.FAIL_CONFIRM_RESET_PASSWORD, errors); - -export const doResetPasswordThunk = (email: string) => (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.resetPassword(email) - .then((result: SigninResult) => { - if (!result.success) { - return dispatch(failResetPassword(result.errors)); - } else { - return dispatch(openConfirmResetPassword(email)); - } - }) - .catch(() => { - return dispatch(failResetPassword({ server: "RequestFailed" })); - }); -}; - -export const doConfirmResetPasswordThunk = - (data: TeacherConfirmResetPasswordData) => (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.confirmResetPassword(data) - .then((result: SigninResult) => { - if (!result.success) { - return dispatch(failConfirmResetPassword(result.errors)); - } else { - doLoginThunk({ - login: data.login, - password: data.password, - })(dispatch); - return dispatch(succeedResetPassword()); - } - }) - .catch(() => { - return dispatch(failResetPassword({ server: "RequestFailed" })); - }); - }; diff --git a/apps/frontend/src/actions/Signin/SignupActions.tsx b/apps/frontend/src/actions/Signin/SignupActions.tsx deleted file mode 100644 index 6cc66b3c..00000000 --- a/apps/frontend/src/actions/Signin/SignupActions.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { - Credentials, - SigninErrors, - SigninResult, - TeacherConfirmData, - TeacherSignupData, -} from "@celluloid/types"; -import { Dispatch } from "redux"; - -import UserService from "~services/UserService"; -import { - ActionType, - createEmptyAction, - createErrorAction, - createOptionalAction, -} from "~types/ActionTypes"; - -import { doLoginThunk, triggerSigninLoading } from "."; - -export const openSignup = () => createEmptyAction(ActionType.OPEN_SIGNUP); - -export const failSignup = (errors: SigninErrors) => - createErrorAction(ActionType.FAIL_SIGNUP, errors); - -export const openConfirmSignup = (credentials?: Credentials) => - createOptionalAction(ActionType.OPEN_CONFIRM_SIGNUP, credentials); - -export const succeedSignup = () => createEmptyAction(ActionType.SUCCEED_SIGNUP); - -export const failConfirmSignup = (errors: SigninErrors) => - createErrorAction(ActionType.FAIL_CONFIRM_SIGNUP, errors); - -export const doSignupThunk = - (data: TeacherSignupData) => (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.signup(data) - .then((result: SigninResult) => { - if (!result.success) { - return dispatch(failSignup(result.errors)); - } else { - return dispatch( - openConfirmSignup({ - login: data.email, - password: data.password, - }) - ); - } - }) - .catch(() => dispatch(failSignup({ server: "RequestFailed" }))); - }; - -export const doConfirmSignupThunk = - (data: TeacherConfirmData, credentials?: Credentials) => - (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.confirmSignup(data) - .then((result: SigninResult) => { - if (!result.success) { - return dispatch(failConfirmSignup(result.errors)); - } else { - if (credentials) { - doLoginThunk(credentials)(dispatch); - } - return dispatch(succeedSignup()); - } - }) - .catch(() => { - return dispatch(failConfirmSignup({ server: "RequestFailed" })); - }); - }; - -export const doResendCodeThunk = (email: string) => (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.resendCode(email) - .then((result: SigninResult) => { - if (!result.success) { - return dispatch(failConfirmSignup(result.errors)); - } else { - return dispatch(succeedSignup()); - } - }) - .catch(() => { - return dispatch(failConfirmSignup({ server: "RequestFailed" })); - }); -}; diff --git a/apps/frontend/src/actions/Signin/StudentSignupActions.tsx b/apps/frontend/src/actions/Signin/StudentSignupActions.tsx deleted file mode 100644 index a6627741..00000000 --- a/apps/frontend/src/actions/Signin/StudentSignupActions.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - SigninErrors, - SigninResult, - StudentSignupData, -} from "@celluloid/types"; -import { Dispatch } from "redux"; - -import UserService from "~services/UserService"; -import { - ActionType, - createEmptyAction, - createErrorAction, - EmptyAction, -} from "~types/ActionTypes"; - -import { doLoginThunk, triggerSigninLoading } from "."; - -export const openStudentSignup = (): EmptyAction => - createEmptyAction(ActionType.OPEN_STUDENT_SIGNUP); - -export const failStudentSignup = (errors: SigninErrors) => - createErrorAction(ActionType.FAIL_STUDENT_SIGNUP, errors); - -export const succeedStudentSignup = (): EmptyAction => - createEmptyAction(ActionType.SUCCEED_STUDENT_SIGNUP); - -export const doStudentSignupThunk = - (data: StudentSignupData) => (dispatch: Dispatch) => { - dispatch(triggerSigninLoading()); - return UserService.studentSignup(data) - .then((result: SigninResult) => { - if (!result.success) { - return dispatch(failStudentSignup(result.errors)); - } else { - doLoginThunk({ - login: data.username, - password: data.password, - })(dispatch); - return dispatch(succeedStudentSignup()); - } - }) - .catch(() => dispatch(failStudentSignup({ server: "RequestFailed" }))); - }; diff --git a/apps/frontend/src/actions/Signin/UserActions.tsx b/apps/frontend/src/actions/Signin/UserActions.tsx deleted file mode 100644 index bdbb5a4c..00000000 --- a/apps/frontend/src/actions/Signin/UserActions.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import { Dispatch } from "redux"; - -import UserService from "~services/UserService"; -import { - Action, - ActionType, - createErrorAction, - createOptionalAction, -} from "~types/ActionTypes"; - -export const failCurrentUser = (error: string): Action => - createErrorAction(ActionType.FAIL_GET_CURRENT_USER, error); - -export const succeedCurrentUser = ( - user?: TeacherRecord -): Action => - createOptionalAction(ActionType.SUCCEED_GET_CURRENT_USER, user); - -export const failLogout = (error: string): Action => - createOptionalAction(ActionType.FAIL_LOGOUT, error); - -export const fetchCurrentUserThunk = () => (dispatch: Dispatch) => { - return UserService.me() - .then((result) => { - if (result.teacher) { - return dispatch(succeedCurrentUser(result.teacher)); - } else { - return dispatch(succeedCurrentUser()); - } - }) - .catch((error) => { - return dispatch(failCurrentUser(error.message)); - }); -}; - -export const doLogoutThunk = () => (dispatch: Dispatch) => { - console.log("doLogoutThunk"); - return UserService.logout() - .then(() => { - console.log("logout result"); - return dispatch(succeedCurrentUser()); - }) - .catch((error) => { - return dispatch(failLogout(error.message)); - }); -}; diff --git a/apps/frontend/src/actions/Signin/index.ts b/apps/frontend/src/actions/Signin/index.ts deleted file mode 100644 index 5f77e17b..00000000 --- a/apps/frontend/src/actions/Signin/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ActionType, createEmptyAction } from "~types/ActionTypes"; - -export const closeSignin = () => createEmptyAction(ActionType.CLOSE_SIGNIN); - -export const triggerSigninLoading = () => - createEmptyAction(ActionType.TRIGGER_SIGNIN_LOADING); - -export * from "./LoginActions"; -export * from "./ResetPasswordActions"; -export * from "./SignupActions"; -export * from "./StudentSignupActions"; -export * from "./UserActions"; diff --git a/apps/frontend/src/components/AppBarMenu.tsx b/apps/frontend/src/components/AppBarMenu.tsx index c0c182c2..c78a9e60 100644 --- a/apps/frontend/src/components/AppBarMenu.tsx +++ b/apps/frontend/src/components/AppBarMenu.tsx @@ -1,82 +1,41 @@ -import { AppBar, Box, Button, styled, Toolbar } from "@mui/material"; +import { AppBar, Box, BoxProps, Button, styled, Toolbar } from "@mui/material"; import * as React from "react"; import { useTranslation } from "react-i18next"; -import { connect, useDispatch } from "react-redux"; import { useLocation, useNavigate } from "react-router"; -import { Link } from "react-router-dom"; -import { Dispatch } from "redux"; -import { - closeSignin, - openLogin, - openSignup, - openStudentSignup, -} from "~actions/Signin"; import { getButtonLink } from "~components/ButtonLink"; import { Footer } from "~components/Footer"; import { LogoWithLabel } from "~components/LogoWithLabel"; -import SigninDialog, { SigninState } from "~components/Signin"; import { SigninMenu } from "~components/SigninMenu"; -import { EmptyAction } from "~types/ActionTypes"; -import { AppState } from "~types/StateTypes"; import { trpc } from "~utils/trpc"; import { LanguageMenu } from "./LanguageMenu"; const Offset = styled("div")(({ theme }) => theme.mixins.toolbar); -type Props = React.PropsWithChildren & { - signinDialog: SigninState; - onClickLogin(): EmptyAction; - onClickSignup(): EmptyAction; - onCloseSignin(): EmptyAction; -}; - -const mapStateToProps = (state: AppState) => { - return { - signinDialog: state.signin.dialog, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onClickLogin: () => dispatch(openLogin()), - onClickSignup: () => dispatch(openSignup()), - onCloseSignin: () => dispatch(closeSignin()), - }; -}; - -export const AppBarMenuWrapper: React.FC = ({ - onClickLogin, - onClickSignup, - onCloseSignin, - signinDialog, - children, -}) => { +export const AppBarMenu: React.FC = ({ children }) => { const { t } = useTranslation(); const navigate = useNavigate(); - const meQuery = trpc.user.me.useQuery(); - const logoutMutation = trpc.user.logout.useMutation(); - const location = useLocation(); + const { data, isError } = trpc.user.me.useQuery(); - const dispatch = useDispatch(); + const location = useLocation(); const handleCreate = () => { - if (!meQuery.error) { + if (!isError) { navigate(`/create`); } else { - dispatch(openStudentSignup()); + navigate("/signup", { state: { backgroundLocation: location } }); } }; const handleJoin = () => { - dispatch(openStudentSignup()); + if (isError) { + navigate("/signup-student", { state: { backgroundLocation: location } }); + } else { + navigate("/join", { state: { backgroundLocation: location } }); + } }; - const handleLogout = async () => { - await logoutMutation.mutateAsync(); - navigate("/"); - }; return ( = ({ {t("menu.about")} - + - {children} ); }; -export const AppBarMenu = connect( - mapStateToProps, - mapDispatchToProps -)(AppBarMenuWrapper); diff --git a/apps/frontend/src/components/Dialog.tsx b/apps/frontend/src/components/Dialog.tsx index 1a08fcd8..1c5494ed 100644 --- a/apps/frontend/src/components/Dialog.tsx +++ b/apps/frontend/src/components/Dialog.tsx @@ -1,5 +1,6 @@ import CloseIcon from "@mui/icons-material/Close"; import { + Alert, Box, DialogTitle, DialogTitleProps, @@ -29,6 +30,7 @@ type StyledDialogProps = DialogProps & { type BootstrapDialogTitleProps = DialogTitleProps & { loading?: boolean; + error?: string | undefined; onClose: () => void; }; @@ -36,12 +38,13 @@ const BootstrapDialogTitle: React.FC = ({ children, onClose, loading, + error, ...other }) => { return ( <> {children} @@ -60,6 +63,11 @@ const BootstrapDialogTitle: React.FC = ({ ) : null} + {error ? ( + + {error} + + ) : null} = ({ onClose={onClose} {...props} > - + {title} - {error ? ( - - - {error} - - - ) : null} {children} diff --git a/apps/frontend/src/components/LoginDialog.tsx b/apps/frontend/src/components/LoginDialog.tsx deleted file mode 100644 index 7863ff6f..00000000 --- a/apps/frontend/src/components/LoginDialog.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogContentText from "@mui/material/DialogContentText"; -import DialogTitle from "@mui/material/DialogTitle"; -import { useNavigate } from "react-router"; - -export const LogintDialog = () => { - const navigate = useNavigate(); - - const handleClose = () => { - navigate(-1); - }; - - return ( - - {"Login"} - - - Let Google help apps determine location. This means sending anonymous - location data to Google, even when no apps are running. - - - - Disagree - - Agree - - - - ); -}; diff --git a/apps/frontend/src/components/SharedLayout.tsx b/apps/frontend/src/components/SharedLayout.tsx index 6b8b13de..d44c776a 100644 --- a/apps/frontend/src/components/SharedLayout.tsx +++ b/apps/frontend/src/components/SharedLayout.tsx @@ -1,42 +1,12 @@ import * as React from "react"; -import { useEffect } from "react"; -import { connect } from "react-redux"; import { Outlet } from "react-router-dom"; -import { AnyAction, Dispatch } from "redux"; - -import { - doLogoutThunk, - fetchCurrentUserThunk, -} from "~actions/Signin/UserActions"; import { AppBarMenu } from "./AppBarMenu"; -type Props = React.PropsWithChildren & { - loadUser(): Promise; - onClickLogout(): Promise; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - loadUser: () => fetchCurrentUserThunk()(dispatch), - onClickLogout: () => doLogoutThunk()(dispatch), - }; -}; - -const SharedLayoutInner: React.FC = ({ onClickLogout, loadUser }) => { - useEffect(() => { - loadUser(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - +export const SharedLayout: React.FC = () => { return ( - + ); }; - -export const SharedLayout = connect( - null, - mapDispatchToProps -)(SharedLayoutInner); diff --git a/apps/frontend/src/components/Signin/ConfirmResetPassword/ConfirmResetPasswordComponent.tsx b/apps/frontend/src/components/Signin/ConfirmResetPassword/ConfirmResetPasswordComponent.tsx deleted file mode 100644 index 9a0c08b2..00000000 --- a/apps/frontend/src/components/Signin/ConfirmResetPassword/ConfirmResetPasswordComponent.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - SigninErrors, - TeacherConfirmResetPasswordData, -} from "@celluloid/types"; -import { TextField } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { AnyAction } from "redux"; - -import DialogButtons from "~components/DialogButtons"; -import DialogError from "~components/DialogError"; - -interface Props { - data: TeacherConfirmResetPasswordData; - errors: SigninErrors; - confirmPasswordError: boolean; - onChange(name: string, value: string): void; - onSubmit(): Promise; -} - -// eslint-disable-next-line import/no-anonymous-default-export -const ConfirmComponent = ({ - data, - errors, - confirmPasswordError, - onChange, - onSubmit, -}: Props) => { - const { t } = useTranslation(); - return ( - - onChange("login", event.target.value)} - helperText={errors && errors.login} - /> - onChange("code", event.target.value)} - helperText={errors.code ? errors.code : t("signin.codeHelper")} - /> - onChange("password", event.target.value)} - helperText={errors && errors.password} - /> - onChange("confirmPassword", event.target.value)} - helperText={ - confirmPasswordError ? t("signin.passwordMismatch") : undefined - } - /> - {errors.server && } - - - ); -}; - -export default ConfirmComponent; diff --git a/apps/frontend/src/components/Signin/ConfirmResetPassword/ConfirmResetPasswordContainer.tsx b/apps/frontend/src/components/Signin/ConfirmResetPassword/ConfirmResetPasswordContainer.tsx deleted file mode 100644 index d0456e72..00000000 --- a/apps/frontend/src/components/Signin/ConfirmResetPassword/ConfirmResetPasswordContainer.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - SigninErrors, - TeacherConfirmResetPasswordData, -} from "@celluloid/types"; -import * as React from "react"; -import { connect } from "react-redux"; -import { AnyAction, Dispatch } from "redux"; - -import { doConfirmResetPasswordThunk } from "~actions/Signin"; -import { Action } from "~types/ActionTypes"; -import { AppState } from "~types/StateTypes"; - -import ConfirmResetPassword from "./ConfirmResetPasswordComponent"; - -interface Props { - errors: SigninErrors; - onClickSignup?(): Action; - onSubmit(data: TeacherConfirmResetPasswordData): Promise; -} - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onSubmit: (data: TeacherConfirmResetPasswordData) => - doConfirmResetPasswordThunk(data)(dispatch), - }; -}; - -const mapStateToProps = (state: AppState) => { - return { - errors: state.signin.errors, - }; -}; - -interface State extends TeacherConfirmResetPasswordData { - confirmPassword: string; -} - -class Confirm extends React.Component { - state = { - login: "", - code: "", - password: "", - confirmPassword: "", - } as State; - - render() { - const onChange = (name: string, value: string) => { - this.setState((state) => ({ - ...state, - [name]: value, - })); - }; - - const confirmPasswordError = - this.state.confirmPassword === this.state.password ? false : true; - - return ( - this.props.onSubmit(this.state)} - onChange={onChange} - /> - ); - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Confirm); diff --git a/apps/frontend/src/components/Signin/ConfirmResetPassword/index.tsx b/apps/frontend/src/components/Signin/ConfirmResetPassword/index.tsx deleted file mode 100644 index b8cbaa68..00000000 --- a/apps/frontend/src/components/Signin/ConfirmResetPassword/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import ConfirmResetPassword from './ConfirmResetPasswordContainer'; - -export default ConfirmResetPassword; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/ConfirmSignup/ConfirmComponent.tsx b/apps/frontend/src/components/Signin/ConfirmSignup/ConfirmComponent.tsx deleted file mode 100644 index 4f9e3a68..00000000 --- a/apps/frontend/src/components/Signin/ConfirmSignup/ConfirmComponent.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { SigninErrors, TeacherConfirmData } from "@celluloid/types"; -import { TextField } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { AnyAction } from "redux"; - -import DialogAltButtons from "~components/DialogAltButtons"; -import DialogButtons from "~components/DialogButtons"; -import SigninError from "~components/DialogError"; - -interface Props { - data: TeacherConfirmData; - errors: SigninErrors; - onChange(name: string, value: string): void; - onClickResend(): Promise; - onSubmit(): Promise; -} - -const ConfirmComponent = ({ - data, - errors, - onChange, - onClickResend, - onSubmit, -}: Props) => { - const { t } = useTranslation(); - - return ( - - onChange("signin.login", event.target.value)} - helperText={errors.login} - /> - onChange("code", event.target.value)} - helperText={errors.code ? errors.code : t("signin.codeHelper")} - /> - {errors.server && } - - - - ); -}; - -export default ConfirmComponent; diff --git a/apps/frontend/src/components/Signin/ConfirmSignup/ConfirmContainer.tsx b/apps/frontend/src/components/Signin/ConfirmSignup/ConfirmContainer.tsx deleted file mode 100644 index 6722c314..00000000 --- a/apps/frontend/src/components/Signin/ConfirmSignup/ConfirmContainer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - Credentials, - SigninErrors, - TeacherConfirmData, -} from "@celluloid/types"; -import * as React from "react"; -import { connect } from "react-redux"; -import { AnyAction, Dispatch } from "redux"; - -import { doConfirmSignupThunk, doResendCodeThunk } from "~actions/Signin"; -import { AppState } from "~types/StateTypes"; - -import ConfirmSignup from "./ConfirmComponent"; - -interface Props { - credentials?: Credentials; - errors: SigninErrors; - onClickResend(email: string): Promise; - onSubmit( - data: TeacherConfirmData, - credentials?: Credentials - ): Promise; -} - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onSubmit: (data: TeacherConfirmData, credentials?: Credentials) => - doConfirmSignupThunk(data, credentials)(dispatch), - onClickResend: (email: string) => doResendCodeThunk(email)(dispatch), - }; -}; - -const mapStateToProps = (state: AppState) => { - return { - errors: state.signin.errors, - credentials: state.signin.credentials, - }; -}; - -class Confirm extends React.Component { - state = { - login: this.props.credentials ? this.props.credentials.login : "", - code: "", - } as TeacherConfirmData; - - render() { - const onChange = (name: string, value: string) => { - this.setState((state) => ({ - ...state, - [name]: value, - })); - }; - - return ( - this.props.onClickResend(this.state.login)} - onSubmit={() => this.props.onSubmit(this.state, this.props.credentials)} - onChange={onChange} - /> - ); - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Confirm); diff --git a/apps/frontend/src/components/Signin/ConfirmSignup/index.tsx b/apps/frontend/src/components/Signin/ConfirmSignup/index.tsx deleted file mode 100644 index 7de5fac7..00000000 --- a/apps/frontend/src/components/Signin/ConfirmSignup/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import Confirm from './ConfirmContainer'; - -export default Confirm; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/DialogComponent.tsx b/apps/frontend/src/components/Signin/DialogComponent.tsx deleted file mode 100644 index cdaeb9b0..00000000 --- a/apps/frontend/src/components/Signin/DialogComponent.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { styled } from "@mui/material"; -import Dialog from "@mui/material/Dialog"; -import DialogContent from "@mui/material/DialogContent"; - -import DialogHeader from "../DialogHeader"; -import ConfirmResetPassword from "./ConfirmResetPassword"; -import ConfirmSignup from "./ConfirmSignup"; -import Login from "./Login"; -import ResetPassword from "./ResetPassword"; -import Signup from "./Signup"; -import StudentSignup from "./StudentSignup"; - -const BootstrapDialog = styled(Dialog)(({ theme }) => ({ - "& .MuiDialogContent-root": { - padding: theme.spacing(2), - }, - "& .MuiDialogActions-root": { - padding: theme.spacing(0), - }, -})); -interface Props { - loading: boolean; - title: string; - open: boolean; - Content?: - | typeof Login - | typeof Signup - | typeof StudentSignup - | typeof ConfirmSignup - | typeof ResetPassword - | typeof ConfirmResetPassword; - onCancel(): void; -} - -const DialogComponent: React.FC = (props) => { - const { loading = false, title, open, onCancel, Content } = props; - - return ( - onCancel()} - > - - {title} - - - {Content && } - - - ); -}; - -export default DialogComponent; diff --git a/apps/frontend/src/components/Signin/DialogContainer.tsx b/apps/frontend/src/components/Signin/DialogContainer.tsx deleted file mode 100644 index 85d75c2d..00000000 --- a/apps/frontend/src/components/Signin/DialogContainer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react"; -import { connect } from "react-redux"; - -import { AppState } from "~types/StateTypes"; - -import ConfirmResetPassword from "./ConfirmResetPassword"; -import ConfirmSignup from "./ConfirmSignup"; -import Dialog from "./DialogComponent"; -import Login from "./Login"; -import ResetPassword from "./ResetPassword"; -import { SigninState } from "./SigninTypes"; -import Signup from "./Signup"; -import StudentSignup from "./StudentSignup"; - -interface Props { - state: SigninState; - loading: boolean; - onCancel(): void; -} - -const getComponent = (state: SigninState) => { - switch (state.kind) { - case "Signup": - return Signup; - case "StudentSignup": - return StudentSignup; - case "Login": - return Login; - case "ConfirmSignup": - return ConfirmSignup; - case "ResetPassword": - return ResetPassword; - case "ConfirmResetPassword": - return ConfirmResetPassword; - default: - return undefined; - } -}; - -const mapStateToProps = (state: AppState) => ({ - loading: state.signin.loading, -}); - -const DialogContainer: React.FC = ({ state, loading, onCancel }) => { - if (state.kind !== "None") { - return ( - - ); - } else { - return ; - } -}; - -export default connect(mapStateToProps)(DialogContainer); diff --git a/apps/frontend/src/components/Signin/Login/LoginComponent.tsx b/apps/frontend/src/components/Signin/Login/LoginComponent.tsx deleted file mode 100644 index 272a682a..00000000 --- a/apps/frontend/src/components/Signin/Login/LoginComponent.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Credentials, SigninErrors } from "@celluloid/types"; -import { Button, DialogActions } from "@mui/material"; -import TextField from "@mui/material/TextField"; -import { useTranslation } from "react-i18next"; -import { AnyAction } from "redux"; - -import DialogAltButtons from "~components/DialogAltButtons"; -import SigninError from "~components/DialogError"; -import { Action } from "~types/ActionTypes"; - -interface Props { - credentials: Credentials; - errors: SigninErrors; - onChange(name: string, value: string): void; - onClickResetPassword(): Action; - onClickSignup(): Action; - onSubmit(): Promise; -} - -const LoginComponent = ({ - credentials, - errors, - onChange, - onClickResetPassword, - onClickSignup, - onSubmit, -}: Props) => { - const { t } = useTranslation(); - return ( - - onChange("login", event.target.value)} - helperText={errors && errors.login} - /> - onChange("password", event.target.value)} - helperText={errors && errors.password} - /> - {errors.server && } - - - - - - {t("signin.forgotPasswordAction")} - - - {t("signin.loginAction")} - - - - ); -}; - -export default LoginComponent; diff --git a/apps/frontend/src/components/Signin/Login/LoginContainer.tsx b/apps/frontend/src/components/Signin/Login/LoginContainer.tsx deleted file mode 100644 index f27131c2..00000000 --- a/apps/frontend/src/components/Signin/Login/LoginContainer.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Credentials, SigninErrors } from "@celluloid/types"; -import * as React from "react"; -import { connect } from "react-redux"; -import { AnyAction, Dispatch } from "redux"; - -import { doLoginThunk, openResetPassword, openSignup } from "~actions/Signin"; -import { Action } from "~types/ActionTypes"; -import { AppState } from "~types/StateTypes"; - -import Login from "./LoginComponent"; - -interface Props { - errors: SigninErrors; - onClickSignup(): Action; - onClickResetPassword(): Action; - onSubmit(credentials: Credentials): Promise; -} - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onSubmit: (credentials: Credentials) => doLoginThunk(credentials)(dispatch), - onClickSignup: () => dispatch(openSignup()), - onClickResetPassword: () => dispatch(openResetPassword()), - }; -}; - -const mapStateToProps = (state: AppState) => { - return { - errors: state.signin.errors, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)( - class extends React.Component { - state = { - login: "", - password: "", - } as Credentials; - - render() { - const onChange = (name: string, value: string) => { - this.setState((state) => ({ - ...state, - [name]: value, - })); - }; - - return ( - this.props.onSubmit(this.state)} - onChange={onChange} - /> - ); - } - } -); diff --git a/apps/frontend/src/components/Signin/Login/index.tsx b/apps/frontend/src/components/Signin/Login/index.tsx deleted file mode 100644 index 7c97087b..00000000 --- a/apps/frontend/src/components/Signin/Login/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import Login from './LoginContainer'; - -export default Login; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/ResetPassword/ResetPasswordComponent.tsx b/apps/frontend/src/components/Signin/ResetPassword/ResetPasswordComponent.tsx deleted file mode 100644 index 3e3eb948..00000000 --- a/apps/frontend/src/components/Signin/ResetPassword/ResetPasswordComponent.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { SigninErrors } from "@celluloid/types"; -import TextField from "@mui/material/TextField"; -import { useTranslation } from "react-i18next"; -import { AnyAction } from "redux"; - -import DialogButtons from "~components/DialogButtons"; -import SigninError from "~components/DialogError"; - -interface Props { - login: string; - errors: SigninErrors; - onChange(value: string): void; - onSubmit(): Promise; -} - -const ResetPasswordComponent = ({ - login, - errors, - onChange, - onSubmit, -}: Props) => { - const { t } = useTranslation(); - return ( - - onChange(event.target.value)} - helperText={errors && errors.login} - /> - {errors.server && } - - - ); -}; - -export default ResetPasswordComponent; diff --git a/apps/frontend/src/components/Signin/ResetPassword/ResetPasswordContainer.tsx b/apps/frontend/src/components/Signin/ResetPassword/ResetPasswordContainer.tsx deleted file mode 100644 index 17c0675d..00000000 --- a/apps/frontend/src/components/Signin/ResetPassword/ResetPasswordContainer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { SigninErrors } from "@celluloid/types"; -import * as React from "react"; -import { connect } from "react-redux"; -import { AnyAction, Dispatch } from "redux"; - -import { doResetPasswordThunk } from "~actions/Signin/ResetPasswordActions"; -import { AppState } from "~types/StateTypes"; - -import ResetPassword from "./ResetPasswordComponent"; - -interface Props { - errors: SigninErrors; - onSubmit(email: string): Promise; -} - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onSubmit: (email: string) => doResetPasswordThunk(email)(dispatch), - }; -}; - -const mapStateToProps = (state: AppState) => { - return { - errors: state.signin.errors, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)( - class extends React.Component { - state = { login: "" }; - - render() { - const onChange = (value: string) => { - this.setState({ - login: value, - }); - }; - - return ( - this.props.onSubmit(this.state.login)} - onChange={onChange} - /> - ); - } - } -); diff --git a/apps/frontend/src/components/Signin/ResetPassword/index.tsx b/apps/frontend/src/components/Signin/ResetPassword/index.tsx deleted file mode 100644 index d49d58ca..00000000 --- a/apps/frontend/src/components/Signin/ResetPassword/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import ResetPassword from './ResetPasswordContainer'; - -export default ResetPassword; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/SigninTypes.tsx b/apps/frontend/src/components/Signin/SigninTypes.tsx deleted file mode 100644 index 4d880334..00000000 --- a/apps/frontend/src/components/Signin/SigninTypes.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { t } from 'i18next'; - -interface SigninStateInterface { - readonly kind: string; - readonly name: string; -} - -export class SignupOpen implements SigninStateInterface { - readonly kind = 'Signup'; - readonly name = t('signin.signupTitle'); -} - -export class ConfirmSignupOpen implements SigninStateInterface { - readonly kind = 'ConfirmSignup'; - readonly name = t('signin.confirmSignupTitle'); -} - -export class LoginOpen implements SigninStateInterface { - readonly kind = 'Login'; - readonly name = t('signin.loginTitle'); -} - -export class ResetPasswordOpen implements SigninStateInterface { - readonly kind = 'ResetPassword'; - readonly name = t('signin.forgotPasswordTitle'); -} - -export class ConfirmResetPasswordOpen implements SigninStateInterface { - readonly kind = 'ConfirmResetPassword'; - readonly name = t('signin.forgotPasswordTitle'); -} - -export class StudentSignupOpen implements SigninStateInterface { - readonly kind = 'StudentSignup'; - readonly name = t('signin.joinProjectTitle'); -} - -export class Closed implements SigninStateInterface { - readonly kind = 'None'; - readonly name = ''; -} - -export type SigninState - = SignupOpen - | ConfirmSignupOpen - | LoginOpen - | ResetPasswordOpen - | ConfirmResetPasswordOpen - | StudentSignupOpen - | Closed; - -export type SigninComponent - = SignupOpen - | ConfirmSignupOpen - | LoginOpen - | ResetPasswordOpen - | StudentSignupOpen - | ConfirmResetPasswordOpen; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/Signup/SignupComponent.tsx b/apps/frontend/src/components/Signin/Signup/SignupComponent.tsx deleted file mode 100644 index 04b38cf5..00000000 --- a/apps/frontend/src/components/Signin/Signup/SignupComponent.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { SigninErrors, TeacherSignupData, UserRecord } from "@celluloid/types"; -import { Button, DialogActions, Typography } from "@mui/material"; -import TextField from "@mui/material/TextField"; -import { useTranslation } from "react-i18next"; -import { AnyAction } from "redux"; - -import DialogAltButtons from "~components/DialogAltButtons"; -import DialogError from "~components/DialogError"; -import { Action } from "~types/ActionTypes"; -import { PeertubeVideoInfo } from "~types/YoutubeTypes"; - -interface Props { - user?: UserRecord; - video?: PeertubeVideoInfo; - data: TeacherSignupData; - errors: SigninErrors; - confirmPasswordError: boolean; - onChange(name: string, value: string): void; - onClickLogin(): Action; - onSubmit(): Promise; -} - -const SignupComponent = ({ - data, - user, - video, - errors, - confirmPasswordError, - onChange, - onSubmit, - onClickLogin, -}: Props) => { - const { t } = useTranslation(); - return ( - - {video && user && ( - - {t("signin.upgradeAccountMessage")} - - )} - {video && !user && ( - - {t("signin.signupOrLoginMessage")} - - )} - onChange("username", event.target.value)} - helperText={errors && errors.username} - /> - onChange("email", event.target.value)} - helperText={errors.email} - /> - onChange("password", event.target.value)} - helperText={errors.password} - /> - onChange("confirmPassword", event.target.value)} - helperText={ - confirmPasswordError ? t("signin.passwordMismatch") : undefined - } - /> - {errors.server && } - - {!user && ( - - )} - - - - {t("signin.signupAction")} - - - - ); -}; - -export default SignupComponent; diff --git a/apps/frontend/src/components/Signin/Signup/SignupContainer.tsx b/apps/frontend/src/components/Signin/Signup/SignupContainer.tsx deleted file mode 100644 index 42d343b9..00000000 --- a/apps/frontend/src/components/Signin/Signup/SignupContainer.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { SigninErrors, TeacherSignupData, UserRecord } from "@celluloid/types"; -import * as React from "react"; -import { connect } from "react-redux"; -import { AnyAction, Dispatch } from "redux"; - -import { doSignupThunk, openLogin } from "~actions/Signin"; -import { Action } from "~types/ActionTypes"; -import { AppState } from "~types/StateTypes"; -import { PeertubeVideoInfo } from "~types/YoutubeTypes"; - -import Signup from "./SignupComponent"; - -interface Props { - user?: UserRecord; - video?: PeertubeVideoInfo; - errors: SigninErrors; - onClickLogin(): Action; - onSubmit(data: TeacherSignupData): Promise; -} - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onSubmit: (data: TeacherSignupData): Promise => - doSignupThunk(data)(dispatch), - onClickLogin: () => dispatch(openLogin()), - }; -}; - -const mapStateToProps = (state: AppState) => { - return { - user: state.user, - errors: state.signin.errors, - }; -}; - -interface State extends TeacherSignupData { - confirmPassword: string; -} - -export default connect( - mapStateToProps, - mapDispatchToProps -)( - class extends React.Component { - state = { - username: this.props.user ? this.props.user.username : "", - email: "", - password: "", - confirmPassword: "", - } as State; - - render() { - const onChange = (name: string, value: string) => { - this.setState((state) => ({ - ...state, - [name]: value, - })); - }; - - const confirmPasswordError = - this.state.confirmPassword === this.state.password ? false : true; - - return ( - this.props.onSubmit(this.state)} - onChange={onChange} - /> - ); - } - } -); diff --git a/apps/frontend/src/components/Signin/Signup/index.tsx b/apps/frontend/src/components/Signin/Signup/index.tsx deleted file mode 100644 index 991b12ef..00000000 --- a/apps/frontend/src/components/Signin/Signup/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import Signup from './SignupContainer'; - -export default Signup; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/StudentSignup/StudentSignupComponent.tsx b/apps/frontend/src/components/Signin/StudentSignup/StudentSignupComponent.tsx deleted file mode 100644 index e9f6128d..00000000 --- a/apps/frontend/src/components/Signin/StudentSignup/StudentSignupComponent.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { SigninErrors, StudentSignupData } from "@celluloid/types"; -import TextField from "@mui/material/TextField"; -import { useTranslation } from "react-i18next"; -import { AnyAction } from "redux"; - -import DialogAltButtons from "~components/DialogAltButtons"; -import DialogButtons from "~components/DialogButtons"; -import DialogError from "~components/DialogError"; -import { Action } from "~types/ActionTypes"; - -// const styles = ({ spacing }: Theme) => -// createStyles({ -// question: { -// marginTop: spacing.unit * 2, -// }, -// }); - -interface Props { - data: StudentSignupData; - errors: SigninErrors; - confirmPasswordError?: string; - onChange(name: string, value: string): void; - onClickLogin(): Action; - onSubmit(): Promise; -} - -const StudentSignupComponent = ({ - data, - errors, - onChange, - onSubmit, - onClickLogin, -}: Props) => { - const { t } = useTranslation(); - return ( - <> - onChange("shareCode", event.target.value)} - helperText={errors && errors.shareCode} - /> - onChange("username", event.target.value)} - helperText={errors.username} - /> - - onChange("password", event.target.value)} - helperText={errors.password ? errors.password : ""} - /> - - {errors.server && } - - - > - ); -}; - -export default StudentSignupComponent; diff --git a/apps/frontend/src/components/Signin/StudentSignup/StudentSignupContainer.tsx b/apps/frontend/src/components/Signin/StudentSignup/StudentSignupContainer.tsx deleted file mode 100644 index b28465cd..00000000 --- a/apps/frontend/src/components/Signin/StudentSignup/StudentSignupContainer.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { SigninErrors, StudentSignupData } from "@celluloid/types"; -import React, { useState } from "react"; -import { connect } from "react-redux"; -import { AnyAction, Dispatch } from "redux"; - -import { doStudentSignupThunk, openLogin } from "~actions/Signin"; -import { Action } from "~types/ActionTypes"; -import { AppState } from "~types/StateTypes"; - -import StudentSignup from "./StudentSignupComponent"; - -interface Props { - errors: SigninErrors; - onClickLogin(): Action; - onSubmit(data: StudentSignupData): Promise; -} - -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - onSubmit: (data: StudentSignupData): Promise => - doStudentSignupThunk(data)(dispatch), - onClickLogin: () => dispatch(openLogin()), - }; -}; - -const mapStateToProps = (state: AppState) => { - return { - errors: state.signin.errors, - }; -}; - -const StudentSignupComponent: React.FC = ({ - errors, - onClickLogin, - onSubmit, -}) => { - const [state, setState] = useState({ - shareCode: "", - username: "", - password: "", - }); - - const handleChange = (name: string, value: string) => { - setState((state) => ({ - ...state, - [name]: value, - })); - }; - - return ( - onSubmit(state)} - onChange={handleChange} - /> - ); -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(StudentSignupComponent); diff --git a/apps/frontend/src/components/Signin/StudentSignup/index.tsx b/apps/frontend/src/components/Signin/StudentSignup/index.tsx deleted file mode 100644 index de6f2da2..00000000 --- a/apps/frontend/src/components/Signin/StudentSignup/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import StudentSignup from './StudentSignupContainer'; - -export default StudentSignup; \ No newline at end of file diff --git a/apps/frontend/src/components/Signin/index.ts b/apps/frontend/src/components/Signin/index.ts deleted file mode 100644 index e50af0df..00000000 --- a/apps/frontend/src/components/Signin/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import SigninDialog from './DialogContainer'; - -export default SigninDialog; -export * from './SigninTypes'; \ No newline at end of file diff --git a/apps/frontend/src/components/SigninMenu.tsx b/apps/frontend/src/components/SigninMenu.tsx index f040b378..6befc6e1 100644 --- a/apps/frontend/src/components/SigninMenu.tsx +++ b/apps/frontend/src/components/SigninMenu.tsx @@ -2,27 +2,25 @@ import { IconButton, Menu, MenuItem } from "@mui/material"; import Button from "@mui/material/Button"; import * as React from "react"; import { Trans } from "react-i18next"; -import { useNavigate } from "react-router"; +import { useLocation, useNavigate } from "react-router"; import { UserAvatar } from "~components/UserAvatar"; -import { UserMe } from "~utils/trpc"; +import { trpc, UserMe } from "~utils/trpc"; interface Props { user?: UserMe; - onClickLogin(): void; - onClickSignup(): void; - onClickLogout(): void; } -export const SigninMenu = ({ - user, - onClickLogin, - onClickSignup, - onClickLogout, -}: Props) => { +export const SigninMenu = ({ user }: Props) => { const navigate = useNavigate(); + const location = useLocation(); + const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); + + const utils = trpc.useContext(); + const mutation = trpc.user.logout.useMutation(); + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -30,9 +28,12 @@ export const SigninMenu = ({ setAnchorEl(null); }; - const handleLogout = () => { + const handleLogout = async () => { + await mutation.mutateAsync(); + utils.user.me.invalidate(); + utils.project.list.invalidate(); handleClose(); - onClickLogout(); + navigate("/"); }; const handleProfile = () => { @@ -45,6 +46,14 @@ export const SigninMenu = ({ handleClose(); }; + const handleLogin = () => { + navigate("/login", { state: { backgroundLocation: location } }); + }; + + const handleSignup = () => { + navigate("/signup", { state: { backgroundLocation: location } }); + }; + return ( {user ? ( @@ -54,7 +63,7 @@ export const SigninMenu = ({ ) : ( )} - {/* - test {location.pathname} - - - test {location.pathname} - */} { - const navigate = useNavigate(); - - const handleClose = () => { - navigate(-1); - }; - - return ( - - {"Signup"} - - - Let Google help apps determine location. This means sending anonymous - location data to Google, even when no apps are running. - - - - Disagree - - Agree - - - - ); -}; diff --git a/apps/frontend/src/components/UpdateIndicator.tsx b/apps/frontend/src/components/UpdateIndicator.tsx index 87793c8a..a22bd2a0 100644 --- a/apps/frontend/src/components/UpdateIndicator.tsx +++ b/apps/frontend/src/components/UpdateIndicator.tsx @@ -2,19 +2,12 @@ import Button from "@mui/material/Button"; import Snackbar from "@mui/material/Snackbar"; import * as React from "react"; import { Trans } from "react-i18next"; -import { connect } from "react-redux"; - -import { AppState } from "~types/StateTypes"; - -const mapStateToProps = (state: AppState) => ({ - open: state.updated, -}); interface Props { open: boolean; } -const UpdateIndicator: React.FC = ({ open }: Props) => ( +export const UpdateIndicator: React.FC = ({ open }: Props) => ( = ({ open }: Props) => ( } /> ); - -export default connect(mapStateToProps)(UpdateIndicator); diff --git a/apps/frontend/src/components/login/ConfirmDialog.tsx b/apps/frontend/src/components/login/ConfirmDialog.tsx new file mode 100644 index 00000000..e2cb054d --- /dev/null +++ b/apps/frontend/src/components/login/ConfirmDialog.tsx @@ -0,0 +1,115 @@ +import { LoadingButton } from "@mui/lab"; +import { Alert, Box, Button, DialogActions, Stack } from "@mui/material"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation, useNavigate, useParams } from "react-router"; +import * as Yup from "yup"; + +import { StyledDialog } from "~components/Dialog"; +import { useRouteQuery } from "~hooks/useRouteQuery"; +import { trpc } from "~utils/trpc"; + +export const ConfirmDialog: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const query = useRouteQuery(); + + const utils = trpc.useContext(); + const mutation = trpc.user.confirm.useMutation(); + + const validationSchema = Yup.object().shape({ + username: Yup.string().required(t("confirm.username.required")), + code: Yup.string() + .min(4, "Code is too short - should be 4 chars minimum.") + .required(t("confirm.code.required")), + }); + + const formik = useFormik({ + initialValues: { + username: query.get("email") || "", + code: "", + error: null, + }, + validateOnMount: false, + validationSchema: validationSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values) => { + try { + await mutation.mutateAsync({ + username: values.username, + code: values.code, + }); + + utils.user.me.invalidate(); + navigate("/"); + formik.setStatus("submited"); + } catch (e) { + formik.setFieldError("error", e.message); + console.log(e); + } + }, + }); + + return ( + navigate(-1)} + error={formik.errors.error} + open={true} + loading={formik.isSubmitting} + > + + + + + + + Envoyer + + + + + ); +}; diff --git a/apps/frontend/src/components/login/ForgotDialog.tsx b/apps/frontend/src/components/login/ForgotDialog.tsx new file mode 100644 index 00000000..c0e015e5 --- /dev/null +++ b/apps/frontend/src/components/login/ForgotDialog.tsx @@ -0,0 +1,104 @@ +import { LoadingButton } from "@mui/lab"; +import { Box, Button, DialogActions } from "@mui/material"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router"; +import * as Yup from "yup"; + +import { StyledDialog } from "~components/Dialog"; +import { trpc } from "~utils/trpc"; + +export const ForgotDialog: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + + const utils = trpc.useContext(); + const mutation = trpc.user.forgot.useMutation(); + + const validationSchema = Yup.object().shape({ + email: Yup.string().required(t("forgot.email.required")), + }); + + const handleRecover = () => { + navigate("/recover", { + state: { backgroundLocation: location }, + replace: true, + }); + }; + + const formik = useFormik({ + initialValues: { + email: "", + error: null, + }, + validateOnMount: false, + validationSchema: validationSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values) => { + try { + await mutation.mutateAsync({ + email: values.email, + }); + + utils.user.me.invalidate(); + handleRecover(); + formik.setStatus("submited"); + } catch (e) { + formik.setFieldError("error", e.message); + console.log(e); + } + }, + }); + + return ( + navigate(-1)} + error={formik.errors.error} + open={true} + loading={formik.isSubmitting} + > + + + + + + + Récupérer un compte + + + + Changer le mot de passe + + + + + + + ); +}; diff --git a/apps/frontend/src/components/login/JoinDialog.tsx b/apps/frontend/src/components/login/JoinDialog.tsx new file mode 100644 index 00000000..82394431 --- /dev/null +++ b/apps/frontend/src/components/login/JoinDialog.tsx @@ -0,0 +1,111 @@ +import { LoadingButton } from "@mui/lab"; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + Stack, +} from "@mui/material"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router"; +import * as Yup from "yup"; + +import { StyledDialog } from "~components/Dialog"; +import { isTRPCClientError, trpc } from "~utils/trpc"; + +export const JoinDialog: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + + const utils = trpc.useContext(); + const mutation = trpc.user.join.useMutation(); + + const validationSchema = Yup.object().shape({ + shareCode: Yup.string().required(), + }); + + const formik = useFormik({ + initialValues: { + shareCode: "", + error: null, + }, + validateOnMount: false, + validationSchema: validationSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values) => { + try { + const data = await mutation.mutateAsync({ + shareCode: values.shareCode, + }); + + formik.setStatus("submited"); + utils.user.me.invalidate(); + + if (data.projectId) { + navigate(`/project/${data.projectId}`); + } else { + navigate("/"); + } + } catch (e) { + if (isTRPCClientError(e)) { + // `cause` is now typed as your router's `TRPCClientError` + formik.setFieldError( + "error", + t("join.error.project-not-found", "Code invalide") + ); + } + + formik.setFieldError("error", e.message); + console.log(e); + } + }, + }); + + return ( + navigate(-1)} + error={formik.errors.error} + open={true} + loading={formik.isSubmitting} + > + + + + + + + Rejoindre + + + + + ); +}; diff --git a/apps/frontend/src/components/login/LoginDialog.tsx b/apps/frontend/src/components/login/LoginDialog.tsx index 0c23966e..425e6677 100644 --- a/apps/frontend/src/components/login/LoginDialog.tsx +++ b/apps/frontend/src/components/login/LoginDialog.tsx @@ -1,23 +1,27 @@ -import { Credentials, SigninErrors } from "@celluloid/types"; import { LoadingButton } from "@mui/lab"; -import { Button, DialogActions } from "@mui/material"; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + Stack, +} from "@mui/material"; import TextField from "@mui/material/TextField"; import { useFormik } from "formik"; import { Trans, useTranslation } from "react-i18next"; -import { useNavigate } from "react-router"; -import { AnyAction } from "redux"; +import { useLocation, useNavigate } from "react-router"; import * as Yup from "yup"; import { StyledDialog } from "~components/Dialog"; -import DialogAltButtons from "~components/DialogAltButtons"; -import SigninError from "~components/DialogError"; -import { Action } from "~types/ActionTypes"; -import { trpc } from "~utils/trpc"; +import { isTRPCClientError, trpc } from "~utils/trpc"; export const LoginDialog: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); + const utils = trpc.useContext(); const mutation = trpc.user.login.useMutation(); const validationSchema = Yup.object().shape({ @@ -25,83 +29,146 @@ export const LoginDialog: React.FC = () => { password: Yup.string().required(t("signin.password.required")), }); + const handlePasswordReset = () => { + navigate("/forgot", { + state: { backgroundLocation: location }, + replace: true, + }); + }; + + const handleConfirm = () => { + navigate("/confirm", { + state: { backgroundLocation: location }, + replace: true, + }); + }; + + const handleSignup = () => { + navigate("/signup", { + state: { backgroundLocation: location }, + replace: true, + }); + }; + const formik = useFormik({ initialValues: { username: "", - password: null, + password: "", + error: null, }, validateOnMount: false, validationSchema: validationSchema, validateOnBlur: true, validateOnChange: true, - onSubmit: (values) => { - formik.setStatus("submited"); + onSubmit: async (values) => { + try { + await mutation.mutateAsync({ + username: values.username, + email: values.email, + password: values.password, + }); + + utils.user.me.invalidate(); + utils.project.list.invalidate(); + navigate(-1); + formik.setStatus("submited"); + } catch (e) { + if (isTRPCClientError(e)) { + // `cause` is now typed as your router's `TRPCClientError` + console.log("e.message", e.message); + if (e.message === "UserNotConfirmed") { + handleConfirm(); + } + } + formik.setFieldError("error", e.message); + } }, - onReset: () => {}, }); return ( navigate(-1)} - error={undefined} + error={formik.errors.error} open={true} loading={formik.isSubmitting} > - - - {/* {errors.server && } */} - - {/* */} - - - {/* - {t("signin.forgotPasswordAction")} - */} + + + + + + + + + + + + + + Confirm + + + + + - - - + + + + diff --git a/apps/frontend/src/components/login/RecoverDialog.tsx b/apps/frontend/src/components/login/RecoverDialog.tsx new file mode 100644 index 00000000..69eb783d --- /dev/null +++ b/apps/frontend/src/components/login/RecoverDialog.tsx @@ -0,0 +1,167 @@ +import { LoadingButton } from "@mui/lab"; +import { Alert, Box, Button, DialogActions, Stack } from "@mui/material"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router"; +import * as Yup from "yup"; + +import { StyledDialog } from "~components/Dialog"; +import { trpc } from "~utils/trpc"; + +export const RecoverDialog: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + + const utils = trpc.useContext(); + const mutation = trpc.user.recover.useMutation(); + + const validationSchema = Yup.object().shape({ + username: Yup.string().required(t("recover.username.required")), + code: Yup.string() + .min(4, "Code is too short - should be 4 chars minimum.") + .required(t("recover.code.required")), + password: Yup.string() + .min(8, "Password is too short - should be 8 chars minimum.") + .required(t("recover.password.required")), + passwordConfirmation: Yup.string() + .oneOf([Yup.ref("password")], "Passwords must match") + .required("Password confirmation is required"), + }); + + const formik = useFormik({ + initialValues: { + username: "", + code: "", + password: "", + passwordConfirmation: "", + error: null, + }, + validateOnMount: false, + validationSchema: validationSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values) => { + try { + await mutation.mutateAsync({ + username: values.username, + password: values.password, + code: values.code, + }); + + utils.user.me.invalidate(); + navigate("/"); + formik.setStatus("submited"); + } catch (e) { + formik.setFieldError("error", e.message); + console.log(e); + } + }, + }); + + return ( + navigate(-1)} + error={formik.errors.error} + open={true} + loading={formik.isSubmitting} + > + + + + + + + + + + Envoyer + + + + + ); +}; diff --git a/apps/frontend/src/components/login/SignupDialog.tsx b/apps/frontend/src/components/login/SignupDialog.tsx new file mode 100644 index 00000000..38b2729b --- /dev/null +++ b/apps/frontend/src/components/login/SignupDialog.tsx @@ -0,0 +1,193 @@ +import { LoadingButton } from "@mui/lab"; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + Stack, +} from "@mui/material"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router"; +import * as Yup from "yup"; + +import { StyledDialog } from "~components/Dialog"; +import { isTRPCClientError, trpc } from "~utils/trpc"; + +export const SignupDialog: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + + const utils = trpc.useContext(); + const mutation = trpc.user.register.useMutation(); + + const validationSchema = Yup.object().shape({ + username: Yup.string().required(), + email: Yup.string().email().required(), + password: Yup.string().min(8).required(), + passwordConfirmation: Yup.string() + .oneOf([Yup.ref("password")], "Passwords must match") + .required(), + }); + + const formik = useFormik({ + initialValues: { + username: "", + email: "", + password: "", + passwordConfirmation: "", + error: null, + }, + validateOnMount: false, + validationSchema: validationSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values) => { + try { + await mutation.mutateAsync({ + username: values.username, + email: values.email, + password: values.password, + }); + + utils.user.me.invalidate(); + navigate(`/confirm?email=${values.email}`, { + state: { backgroundLocation: location }, + }); + formik.setStatus("submited"); + } catch (e) { + if (isTRPCClientError(e)) { + // `cause` is now typed as your router's `TRPCClientError` + if (e.message === "ACCOUNT_EXISTS") { + formik.setFieldError( + "error", + t("signup.error.account_exists", "Email exists dejà") + ); + } + } + + formik.setFieldError("error", e.message); + console.log(e); + } + }, + }); + + return ( + navigate(-1)} + error={formik.errors.error} + open={true} + loading={formik.isSubmitting} + > + {/* {video && user && ( + + {t("signin.upgradeAccountMessage")} + + )} + {video && !user && ( + + {t("signin.signupOrLoginMessage")} + + )} */} + + + + + + + + + + + + S'inscrire + + + + + ); +}; diff --git a/apps/frontend/src/components/login/StudentSignupDialog.tsx b/apps/frontend/src/components/login/StudentSignupDialog.tsx new file mode 100644 index 00000000..0da8b1bd --- /dev/null +++ b/apps/frontend/src/components/login/StudentSignupDialog.tsx @@ -0,0 +1,190 @@ +import { LoadingButton } from "@mui/lab"; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + Stack, +} from "@mui/material"; +import TextField from "@mui/material/TextField"; +import { useFormik } from "formik"; +import { Trans, useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router"; +import * as Yup from "yup"; + +import { StyledDialog } from "~components/Dialog"; +import { isTRPCClientError, trpc } from "~utils/trpc"; + +export const StudentSignupDialog: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + + const utils = trpc.useContext(); + const mutation = trpc.user.registerAsStudent.useMutation(); + + const validationSchema = Yup.object().shape({ + shareCode: Yup.string().required(), + username: Yup.string().min(4).required(), + password: Yup.string().min(8).required(), + }); + + const handleSignin = () => { + navigate(`/login`, { + state: { backgroundLocation: location }, + }); + }; + + const formik = useFormik({ + initialValues: { + username: "", + password: "", + shareCode: "", + error: null, + }, + validateOnMount: false, + validationSchema: validationSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: async (values) => { + try { + const data = await mutation.mutateAsync({ + username: values.username, + password: values.password, + shareCode: values.shareCode, + }); + + formik.setStatus("submited"); + utils.user.me.invalidate(); + + if (data.projectId) { + navigate(`/project/${data.projectId}`); + } else { + navigate("/"); + } + } catch (e) { + if (isTRPCClientError(e)) { + // `cause` is now typed as your router's `TRPCClientError` + if (e.message === "ACCOUNT_EXISTS") { + formik.setFieldError( + "error", + t( + "student-student-signup.error.username-exists", + "Email exists dejà" + ) + ); + } + } + + formik.setFieldError("error", e.message); + console.log(e); + } + }, + }); + + return ( + navigate(-1)} + error={formik.errors.error} + open={true} + loading={formik.isSubmitting} + > + + + + + + + + + + + + + + Se connecter + + + + + + Rejoindre + + + + + + ); +}; diff --git a/apps/frontend/src/hooks/useRouteQuery.ts b/apps/frontend/src/hooks/useRouteQuery.ts new file mode 100644 index 00000000..e9c310dc --- /dev/null +++ b/apps/frontend/src/hooks/useRouteQuery.ts @@ -0,0 +1,8 @@ +import React from "react"; +import { useLocation } from "react-router-dom"; + +export function useRouteQuery() { + const { search } = useLocation(); + + return React.useMemo(() => new URLSearchParams(search), [search]); +} diff --git a/apps/frontend/src/pages/create.tsx b/apps/frontend/src/pages/create.tsx index f20c6855..ed06b107 100644 --- a/apps/frontend/src/pages/create.tsx +++ b/apps/frontend/src/pages/create.tsx @@ -29,12 +29,12 @@ import * as Yup from "yup"; import { AddVideoToPlaylistDialog } from "~components/AddVideoToPlaylistDialog"; import { StyledTitle } from "~components/typography"; -import { ERR_ALREADY_EXISTING_PROJECT } from "~services/Constants"; import { getPeerTubeVideoData, PeerTubeVideoDataResult, PeerTubeVideoWithThumbnail, } from "~services/peertube"; +import { ERR_ALREADY_EXISTING_PROJECT } from "~utils/Constants"; // import { formatDuration } from "~utils/DurationUtils"; import { humanizeError } from "~utils/errors"; import { trpc } from "~utils/trpc"; diff --git a/apps/frontend/src/pages/home.tsx b/apps/frontend/src/pages/home.tsx index 16dcaee9..da2057e0 100644 --- a/apps/frontend/src/pages/home.tsx +++ b/apps/frontend/src/pages/home.tsx @@ -16,7 +16,7 @@ import { Suspense } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { openStudentSignup } from "~actions/Signin"; import { ProjectGrid } from "~components/home/ProjectGrid"; @@ -27,21 +27,21 @@ import { trpc } from "~utils/trpc"; export const HomePage: React.FC = () => { const { isError } = trpc.user.me.useQuery(); + const location = useLocation(); const { t } = useTranslation(); const navigate = useNavigate(); - const dispatch = useDispatch(); const handleJoin = () => { - dispatch(openStudentSignup()); + if (isError) { + navigate("/signup-student", { state: { backgroundLocation: location } }); + } else { + navigate("/join", { state: { backgroundLocation: location } }); + } }; const handleCreate = () => { - if (!isError) { - navigate(`/create`); - } else { - dispatch(openStudentSignup()); - } + navigate(`/create`); }; return ( diff --git a/apps/frontend/src/reducers/SharingReducer.tsx b/apps/frontend/src/reducers/SharingReducer.tsx deleted file mode 100644 index 6ae72522..00000000 --- a/apps/frontend/src/reducers/SharingReducer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { AnyAction } from "redux"; - -import { ActionType } from "~types/ActionTypes"; -import { SharingState, SharingStatus } from "~types/StateTypes"; - -const initialState = { - error: undefined, - status: SharingStatus.CLOSED, -} as SharingState; - -export default (state = initialState, action: AnyAction): SharingState => { - switch (action.type) { - case ActionType.TRIGGER_SHARE_PROJECT_LOADING: - return { - status: SharingStatus.LOADING, - error: undefined, - }; - case ActionType.SUCCEED_SHARE_PROJECT: - return { - status: SharingStatus.CLOSED, - error: undefined, - }; - case ActionType.FAIL_SHARE_PROJECT: - return { - status: SharingStatus.ERROR, - error: action.payload, - }; - case ActionType.OPEN_SHARE_PROJECT: - return { - status: SharingStatus.OPEN, - error: undefined, - }; - case ActionType.CANCEL_SHARE_PROJECT: - return { - status: SharingStatus.CLOSED, - error: undefined, - }; - default: - return state; - } -}; diff --git a/apps/frontend/src/reducers/SigninReducer.tsx b/apps/frontend/src/reducers/SigninReducer.tsx deleted file mode 100644 index 8bd60b4b..00000000 --- a/apps/frontend/src/reducers/SigninReducer.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { AnyAction } from "redux"; - -import * as SigninDialog from "~components/Signin"; -import { ActionType } from "~types/ActionTypes"; - -const initialState = { - loading: false, - errors: {}, - dialog: new SigninDialog.Closed(), - email: undefined, - password: undefined, -}; - -export default (state = initialState, action: AnyAction) => { - switch (action.type) { - case ActionType.OPEN_LOGIN: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.LoginOpen(), - }; - case ActionType.OPEN_SIGNUP: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.SignupOpen(), - }; - case ActionType.OPEN_STUDENT_SIGNUP: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.StudentSignupOpen(), - }; - case ActionType.OPEN_CONFIRM_SIGNUP: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.ConfirmSignupOpen(), - credentials: action.payload, - }; - case ActionType.OPEN_RESET_PASSWORD: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.ResetPasswordOpen(), - }; - case ActionType.OPEN_CONFIRM_RESET_PASSWORD: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.ConfirmResetPasswordOpen(), - }; - case ActionType.TRIGGER_SIGNIN_LOADING: - return { - ...state, - loading: true, - }; - case ActionType.CLOSE_SIGNIN: - return { - loading: false, - errors: {}, - dialog: new SigninDialog.Closed(), - }; - case ActionType.FAIL_LOGIN: - case ActionType.FAIL_SIGNUP: - case ActionType.FAIL_CONFIRM_SIGNUP: - case ActionType.FAIL_STUDENT_SIGNUP: - return { - ...state, - loading: false, - errors: action.payload, - }; - case ActionType.SUCCEED_LOGIN: - return { - loading: false, - dialog: new SigninDialog.Closed(), - errors: {}, - }; - case ActionType.SUCCEED_SIGNUP: - return { - ...state, - loading: false, - errors: {}, - dialog: new SigninDialog.LoginOpen(), - }; - default: - return state; - } -}; diff --git a/apps/frontend/src/reducers/UserReducer.tsx b/apps/frontend/src/reducers/UserReducer.tsx deleted file mode 100644 index 1bdc0a8f..00000000 --- a/apps/frontend/src/reducers/UserReducer.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { AnyAction } from "redux"; - -import { ActionType } from "~types/ActionTypes"; - -const initialState = null; - -export default (state = initialState, action: AnyAction) => { - switch (action.type) { - case ActionType.FAIL_GET_CURRENT_USER: - return null; - case ActionType.SUCCEED_GET_CURRENT_USER: - return action.payload; - case ActionType.FAIL_LOGOUT: - return initialState; - default: - return state; - } -}; diff --git a/apps/frontend/src/reducers/index.tsx b/apps/frontend/src/reducers/index.tsx deleted file mode 100644 index 99930c88..00000000 --- a/apps/frontend/src/reducers/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { connectRouter } from "connected-react-router"; -import { History } from "history"; -import { AnyAction, combineReducers } from "redux"; - -import { ActionType } from "~types/ActionTypes"; - -import sharing from "./SharingReducer"; -import signin from "./SigninReducer"; -import user from "./UserReducer"; - -const updatedReducer = (state = false, action: AnyAction): boolean => { - switch (action.type) { - case ActionType.APPLICATION_UPDATED: - return true; - default: - return state; - } -}; - -const rootReducer = (history: History) => - combineReducers({ - signin, - user, - sharing, - updated: updatedReducer, - router: connectRouter(history), - }); - -export default rootReducer; diff --git a/apps/frontend/src/services/UserService.ts b/apps/frontend/src/services/UserService.ts deleted file mode 100644 index 8103caf8..00000000 --- a/apps/frontend/src/services/UserService.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { - Credentials, - StudentSignupData, - TeacherConfirmData, - TeacherConfirmResetPasswordData, - TeacherSignupData, -} from "@celluloid/types"; - -import * as Constants from "./Constants"; - -export default class { - static login(credentials: Credentials) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - return fetch(`/api/users/login`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify(credentials), - }).then((response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 401) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static signup(data: TeacherSignupData) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - - return fetch(`/api/users/signup`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify(data), - }).then((response) => { - if (response.status === 201) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 409) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static studentSignup(data: StudentSignupData) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - - return fetch(`/api/users/student-signup`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify(data), - }).then((response) => { - if (response.status === 201) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 409) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static confirmSignup(data: TeacherConfirmData) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - - return fetch(`/api/users/confirm-signup`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify(data), - }).then((response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 401) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static resetPassword(email: string) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - - return fetch(`/api/users/reset-password`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify({ email }), - }).then((response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 401) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static confirmResetPassword(data: TeacherConfirmResetPasswordData) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - - return fetch(`/api/users/confirm-reset-password`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify(data), - }).then((response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 401) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static resendCode(email: string) { - const headers = { - Accepts: "application/json", - "Content-type": "application/json", - }; - - return fetch(`/api/users/resend-code`, { - method: "POST", - headers: new Headers(headers), - credentials: "include", - body: JSON.stringify({ email }), - }).then((response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 400) { - return response.json(); - } else if (response.status === 401) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static me() { - const headers = { - Accepts: "application/json", - }; - - return fetch(`/api/users/me`, { - method: "GET", - headers: new Headers(headers), - credentials: "include", - }).then((response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 401) { - return response.json(); - } - throw new Error(Constants.ERR_UNAVAILABLE); - }); - } - - static logout() { - return fetch(`/api/users/logout`, { - method: "PUT", - credentials: "include", - }); - } -} diff --git a/apps/frontend/src/store/index.tsx b/apps/frontend/src/store/index.tsx deleted file mode 100644 index 6a8e99e9..00000000 --- a/apps/frontend/src/store/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { routerMiddleware } from "connected-react-router"; -import { createBrowserHistory } from "history"; -import { applyMiddleware, compose, createStore } from "redux"; - -import createRootReducer from "../reducers"; - -export const history = createBrowserHistory(); - -export default function createAppStore(preloadedState?: any) { - const store = createStore( - createRootReducer(history), - preloadedState, - compose(applyMiddleware(routerMiddleware(history))) - ); - - // Hot reloading - // if (module.hot) { - // // Enable Webpack hot module replacement for reducers - // module.hot.accept("../reducers", () => { - // store.replaceReducer(createRootReducer(history)); - // }); - // } - - return store; -} diff --git a/apps/frontend/src/types/ActionTypes.tsx b/apps/frontend/src/types/ActionTypes.tsx deleted file mode 100644 index d7c953d8..00000000 --- a/apps/frontend/src/types/ActionTypes.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { Action as ReduxAction } from 'redux'; - -export enum ActionType { - - APPLICATION_UPDATED = 'APPLICATION_UPDATED', - OPEN_LOGIN = 'OPEN_LOGIN', - SUCCEED_LOGIN = 'SUCCEED_LOGIN', - FAIL_LOGIN = 'FAIL_LOGIN', - - OPEN_SIGNUP = 'OPEN_SIGNUP', - FAIL_SIGNUP = 'FAIL_SIGNUP', - OPEN_CONFIRM_SIGNUP = 'OPEN_CONFIRM_SIGNUP', - FAIL_CONFIRM_SIGNUP = 'FAIL_CONFIRM_SIGNUP', - SUCCEED_SIGNUP = 'SUCCEED_SIGNUP', - - OPEN_STUDENT_SIGNUP = 'OPEN_STUDENT_SIGNUP', - FAIL_STUDENT_SIGNUP = 'FAIL_STUDENT_SIGNUP', - SUCCEED_STUDENT_SIGNUP = 'SUCCEED_STUDENT_SIGNUP', - - OPEN_RESET_PASSWORD = 'OPEN_RESET_PASSWORD', - FAIL_RESET_PASSWORD = 'FAIL_RESET_PASSWORD', - OPEN_CONFIRM_RESET_PASSWORD = 'OPEN_CONFIRM_RESET_PASSWORD', - FAIL_CONFIRM_RESET_PASSWORD = 'FAIL_CONFIRM_RESET_PASSWORD', - SUCCEED_RESET_PASSWORD = 'SUCCEED_RESET_PASSWORD', - - CLOSE_SIGNIN = 'CLOSE_SIGNIN', - TRIGGER_SIGNIN_LOADING = 'TRIGGER_SIGNIN_LOADING', - - FAIL_GET_CURRENT_USER = 'FAILED_CURRENT_USER', - SUCCEED_GET_CURRENT_USER = 'SUCCESS_CURRENT_USER', - - FAIL_LOGOUT = 'FAIL_LOGOUT', - - FAIL_LOAD_VIDEO = 'FAIL_LOAD_VIDEO', - SUCCEED_LOAD_VIDEO = 'SUCCEED_LOAD_VIDEO', - DISCARD_NEW_VIDEO = 'DISCARD_NEW_VIDEO', - - FAIL_LIST_PROJECTS = 'FAIL_LIST_PROJECTS', - SUCCEED_LIST_PROJECTS = 'SUCCEED_LIST_PROJECTS', - - TRIGGER_LIST_TAGS = 'TRIGGER_LIST_TAGS', - FAIL_LIST_TAGS = 'FAIL_LIST_TAGS', - SUCCEED_LIST_TAGS = 'SUCCEED_LIST_TAGS', - TRIGGER_INSERT_TAG = 'TRIGGER_INSERT_TAG', - SUCCEED_INSERT_TAG = 'SUCCEED_INSERT_TAG', - FAIL_INSERT_TAG = 'FAIL_INSERT_TAG', - - CLEAR_PROJECT = 'CLEAR_PROJECT', - - FAIL_LOAD_PROJECT = 'LOAD_PROJECT', - SUCCEED_LOAD_PROJECT = 'SUCCEED_LOAD_PROJECT', - - TRIGGER_UPSERT_PROJECT_LOADING = 'TRIGGER_UPSERT_PROJECT_LOADING', - FAIL_UPSERT_PROJECT = 'FAIL_UPSERT_PROJECT', - SUCCEED_UPSERT_PROJECT = 'SUCCEED_UPSERT_PROJECT', - - TRIGGER_DELETE_PROJECT_LOADING = 'TRIGGER_DELETE_PROJECT_LOADING', - FAIL_DELETE_PROJECT = 'FAIL_DELETE_PROJECT', - SUCCEED_DELETE_PROJECT = 'SUCCEED_DELETE_PROJECT', - - OPEN_SHARE_PROJECT = 'OPEN_SHARE_PROJECT', - CANCEL_SHARE_PROJECT = 'CANCEL_SHARE_PROJECT', - - TRIGGER_SHARE_PROJECT_LOADING = 'TRIGGER_SHARE_PROJECT_LOADING', - FAIL_SHARE_PROJECT = 'FAIL_SHARE_PROJECT', - SUCCEED_SHARE_PROJECT = 'SUCCEED_SHARE_PROJECT', - - TRIGGER_UNSHARE_PROJECT_LOADING = 'TRIGGER_UNSHARE_PROJECT_LOADING', - FAIL_UNSHARE_PROJECT = 'FAIL_UNSHARE_PROJECT', - SUCCEED_UNSHARE_PROJECT = 'SUCCEED_UNSHARE_PROJECT', - - TRIGGER_SET_PROJECT_PUBLIC_LOADING = 'TRIGGER_SET_PROJECT_PUBLIC_LOADING', - FAIL_SET_PROJECT_PUBLIC = 'FAIL_SET_PROJECT_PUBLIC', - SUCCEED_SET_PROJECT_PUBLIC = 'SUCCEED_SET_PROJECT_PUBLIC', - - TRIGGER_SET_PROJECT_COLLABORATIVE_LOADING = 'TRIGGER_SET_PROJECT_COLLABORATIVE_LOADING', - FAIL_SET_PROJECT_COLLABORATIVE = 'FAIL_SET_PROJECT_COLLABORATIVE', - SUCCEED_SET_PROJECT_COLLABORATIVE = 'SUCCEED_SET_PROJECT_COLLABORATIVE', - - TRIGGER_LIST_ANNOTATIONS_LOADING = 'TRIGGER_LIST_ANNOTATIONS_LOADING', - FAIL_LIST_ANNOTATIONS = 'FAIL_LIST_ANNOTATIONS', - SUCCEED_LIST_ANNOTATIONS = 'SUCCEED_LIST_ANNOTATIONS', - - TRIGGER_FOCUS_ANNOTATION = 'TRIGGER_FOCUS_ANNOTATION', - TRIGGER_BLUR_ANNOTATION = 'TRIGGER_BLUR_ANNOTATION', - TRIGGER_EDIT_ANNOTATION = 'TRIGGER_EDIT_ANNOTATION', - TRIGGER_ADD_ANNOTATION = 'TRIGGER_NEW_ANNOTATION', - TRIGGER_CANCEL_ANNOTATION = 'TRIGGER_CANCEL_ANNOTATION', - - TRIGGER_UPSERT_ANNOTATION_LOADING = 'TRIGGER_UPSERT_ANNOTATION_LOADING', - FAIL_UPSERT_ANNOTATION = 'FAIL_UPSERT_ANNOTATION', - SUCCEED_ADD_ANNOTATION = 'SUCCEED_ADD_ANNOTATION', - SUCCEED_UPDATE_ANNOTATION = 'SUCCEED_UPDATE_ANNOTATION', - - TRIGGER_LIST_COMMENTS_LOADING = 'TRIGGER_LIST_COMMENTS_LOADING', - FAIL_LIST_COMMENTS = 'FAIL_LIST_COMMENTS', - SUCCEED_LIST_COMMENTS = 'SUCCEED_LIST_COMMENTS', - - TRIGGER_DELETE_ANNOTATION_LOADING = 'TRIGGER_DELETE_ANNOTATION_LOADING', - FAIL_DELETE_ANNOTATION = 'FAIL_DELETE_ANNOTATION', - SUCCEED_DELETE_ANNOTATION = 'SUCCEED_DELETE_ANNOTATION', - - TRIGGER_UPSERT_COMMENT_LOADING = 'TRIGGER_UPSERT_COMMENT_LOADING', - FAIL_UPSERT_COMMENT = 'FAIL_UPSERT_COMMENT', - SUCCEED_ADD_COMMENT = 'SUCCEED_ADD_COMMENT', - SUCCEED_UPDATE_COMMENT = 'SUCCEED_UPDATE_COMMENT', - - TRIGGER_DELETE_COMMENT_LOADING = 'TRIGGER_DELETE_COMMENT_LOADING', - FAIL_DELETE_COMMENT = 'FAIL_DELETE_COMMENT', - SUCCEED_DELETE_COMMENT = 'SUCCEED_DELETE_COMMENT', - - TRIGGER_EDIT_COMMENT = 'TRIGGER_EDIT_COMMENT', - TRIGGER_ADD_COMMENT = 'TRIGGER_ADD_COMMENT', - TRIGGER_CANCEL_EDIT_COMMENT = 'TRIGGER_CANCEL_EDIT_COMMENT', - - PLAYER_REQUEST_SEEK = 'PLAYER_REQUEST_SEEK', - PLAYER_NOTIFY_SEEK = 'PLAYER_NOTIFY_SEEK', -} - -export interface Action extends ReduxAction { - type: ActionType; - payload?: T; - error: boolean; -} - -export interface EmptyAction extends Action { } - -export type AsyncAction = Promise>>; - -export function createAction(type: ActionType, payload: P): - Required> { - return { type, payload, error: false }; -} - -export function createOptionalAction(type: ActionType, payload?: P): - Action { - return { type, payload, error: false }; -} - -export function createEmptyAction(type: ActionType): - EmptyAction { - return { type, error: false }; -} - -export function createErrorAction(type: ActionType, payload: P): - Required> { - return { type, payload, error: true }; -} \ No newline at end of file diff --git a/apps/frontend/src/types/LevelTypes.tsx b/apps/frontend/src/types/LevelTypes.tsx deleted file mode 100644 index 6502226c..00000000 --- a/apps/frontend/src/types/LevelTypes.tsx +++ /dev/null @@ -1,34 +0,0 @@ -enum Level { - KINDERGARTEN, - ELEMENTARY_SCHOOL_1, - ELEMENTARY_SCHOOL_2, - MIDDLE_SCHOOL, - HIGH_SCHOOL, - HIGHER_EDUCATION, - RESEARCH -} - -const levelLabel = (level: Level) => { - switch (level) { - case Level.KINDERGARTEN: - return 'levels.kinderGarten'; - case Level.ELEMENTARY_SCHOOL_1: - return 'levels.elementarySchool1'; - case Level.ELEMENTARY_SCHOOL_2: - return 'levels.elementarySchool2'; - case Level.MIDDLE_SCHOOL: - return 'levels.middleSchool'; - case Level.HIGH_SCHOOL: - return 'levels.highSchool'; - case Level.HIGHER_EDUCATION: - return 'levels.higherEducation'; - case Level.RESEARCH: - return 'levels.research'; - default: - return ''; - } -}; - -const levelsCount = Object.keys(Level).length / 2; - -export { Level, levelLabel, levelsCount }; diff --git a/apps/frontend/src/types/ProjectTypes.tsx b/apps/frontend/src/types/ProjectTypes.tsx deleted file mode 100644 index 0d275cd1..00000000 --- a/apps/frontend/src/types/ProjectTypes.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export interface ProjectRouteParams { - projectId: string; -} \ No newline at end of file diff --git a/apps/frontend/src/types/StateTypes.tsx b/apps/frontend/src/types/StateTypes.tsx deleted file mode 100644 index 511e6a6f..00000000 --- a/apps/frontend/src/types/StateTypes.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - AnnotationRecord, - CommentRecord, - Credentials, - ProjectGraphRecord, - SigninErrors, - TagData, - UserRecord, -} from "@celluloid/types"; -import { RouterState } from "connected-react-router"; - -import * as SigninDialog from "~components/Signin"; - -import { PeertubeVideoInfo } from "./YoutubeTypes"; - -export interface SigninState { - loading: boolean; - dialog: SigninDialog.SigninState; - errors: SigninErrors; - credentials?: Credentials; -} - -export interface VideoState { - status: ComponentStatus; - loadingError?: boolean; - annotations: AnnotationRecord[]; - editing: boolean; - commenting: boolean; - annotationError?: string; - focusedAnnotation?: AnnotationRecord; - upsertAnnotationLoading: boolean; - deleteAnnotationLoading: boolean; - commentError?: string; - focusedComment?: CommentRecord; - upsertCommentLoading: boolean; - deleteCommentLoading: boolean; -} - -export interface ProjectDetailsState { - status: ComponentStatus; - error?: string; - project?: ProjectGraphRecord; - setPublicLoading: boolean; - setCollaborativeLoading: boolean; - unshareLoading: boolean; - deleteLoading: boolean; - setPublicError?: string; - setCollaborativeError?: string; - unshareError?: string; - deleteError?: string; -} - -export interface PlayerState { - seeking: boolean; - seekTarget: number; -} - -export interface ProjectState { - player: PlayerState; - video: VideoState; - details: ProjectDetailsState; -} - -export enum SharingStatus { - OPEN, - ERROR, - LOADING, - CLOSED, -} - -export enum ComponentStatus { - LOADING, - ERROR, - READY, -} - -export interface HomeState { - errors: { - projects?: string; - video?: string; - createProject?: string; - }; - projects: ProjectGraphRecord[]; - video?: PeertubeVideoInfo; - createProjectLoading: boolean; -} - -export interface SharingState { - status: SharingStatus; - error?: string; -} - -export interface AppState extends RouterState { - tags: TagData[]; - sharing: SharingState; - project: ProjectState; - home: HomeState; - user?: UserRecord; - signin: SigninState; - updated: boolean; -} diff --git a/apps/frontend/src/types/YoutubeTypes.tsx b/apps/frontend/src/types/YoutubeTypes.tsx deleted file mode 100644 index 12e59826..00000000 --- a/apps/frontend/src/types/YoutubeTypes.tsx +++ /dev/null @@ -1,32 +0,0 @@ -export interface PeertubeVideoInfo { - id: string; - title: string; - thumbnailUrl: string; - host:string; -} - -export interface Player { - getCurrentTime(): number; - getDuration(): number; - playVideo(): void; - pauseVideo(): void; - seekTo(position: number, allowSeekAhead: boolean): void; -} - -export interface PlayerReadyEvent { - target: Player; -} - -export interface PlayerChangeEvent { - target: Player; - data: number; -} - -export enum PlayerEventData { - UNSTARTED = -1, - ENDED = 0, - PLAYING = 1, - PAUSED = 2, - BUFFERING = 3, - CUED = 5 -} \ No newline at end of file diff --git a/apps/frontend/src/services/Constants.ts b/apps/frontend/src/utils/Constants.ts similarity index 100% rename from apps/frontend/src/services/Constants.ts rename to apps/frontend/src/utils/Constants.ts diff --git a/apps/server/package.json b/apps/server/package.json deleted file mode 100644 index ebed9540..00000000 --- a/apps/server/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "server", - "version": "2.0.0", - "description": "Celluloid backend", - "repository": "http://github.com/celluloid-camp/celluloid", - "author": "Younes Benaomar ", - "license": "MIT", - "private": true, - "dependencies": { - "bcryptjs": "^2.4.3", - "body-parser": "^1.18.2", - "change-case": "^4.1.2", - "compression": "^1.7.2", - "connect-redis": "^7.1.0", - "cookie-parser": "^1.4.3", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "express-pino-logger": "^7.0.0", - "express-session": "^1.17.3", - "extend": "^3.0.2", - "js2xmlparser": "^5.0.0", - "knex": "^2.3.0", - "lodash": "^4.17.21", - "mem": "^4.0.0", - "nodemailer": "^6.0.0", - "nodemailer-smtp-transport": "^2.7.4", - "papaparse": "^5.4.1", - "passport": "^0.6.0", - "passport-local": "^1.0.0", - "pg": "^8.8.0", - "pino": "^8.7.0", - "pino-std-serializers": "^6.0.0", - "ramda": "^0.28.0", - "redis": "^4.6.10", - "source-map-support": "^0.5.13", - "tslib": "^2.2.0", - "unfurl.js": "^6.3.1", - "validator": "^13.7.0" - }, - "scripts": { - "build": "tsup", - "dev": "dotenv -e ../../.env -- tsup --watch --silent --onSuccess 'node dist/index.js'", - "start": "node --use-strict dist/index.js" - }, - "devDependencies": { - "@celluloid/config": "*", - "@celluloid/types": "*", - "@celluloid/validators": "*", - "@types/bcrypt": "^3.0.0", - "@types/bcryptjs": "^2.4.2", - "@types/compression": "^1.0.0", - "@types/cookie-parser": "^1.4.1", - "@types/dotenv": "^6.0.0", - "@types/express": "^4.0.39", - "@types/express-pino-logger": "^4.0.2", - "@types/express-session": "^1.15.8", - "@types/knex": "^0.16.1", - "@types/node": "^18.13.0", - "@types/node-fetch": "^2.6.2", - "@types/nodemailer": "^6.4.6", - "@types/nodemailer-smtp-transport": "^2.7.4", - "@types/papaparse": "^5.3.7", - "@types/passport": "^1.0.0", - "@types/passport-local": "^1.0.33", - "@types/pg": "^7.4.1", - "@types/ramda": "^0.25.35", - "dotenv-cli": "^7.2.1", - "jest": "^27.0.1", - "knex-types": "^0.4.0", - "mock-req": "^0.2.0", - "pino-pretty": "^9.1.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" - } -} diff --git a/apps/server/src/Config.ts b/apps/server/src/Config.ts deleted file mode 100644 index 51440f5a..00000000 --- a/apps/server/src/Config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as dotEnv from 'dotenv'; - -import { envFile } from './Paths'; - -dotEnv.config({ path: envFile}); \ No newline at end of file diff --git a/apps/server/src/Paths.ts b/apps/server/src/Paths.ts deleted file mode 100644 index 26d324f6..00000000 --- a/apps/server/src/Paths.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as path from "path"; - -export const rootDir = path.resolve(__dirname, "..", "..", ".."); -export const envFile = path.resolve(rootDir, ".env"); -export const clientDir = path.resolve(rootDir, "apps", "client", "dist"); -export const publicDir = path.resolve(__dirname, "..", "public"); -export const clientApp = path.resolve(clientDir, "index.html"); diff --git a/apps/server/src/api/AnnotationApi.ts b/apps/server/src/api/AnnotationApi.ts deleted file mode 100644 index ab35d7df..00000000 --- a/apps/server/src/api/AnnotationApi.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { AnnotationData, AnnotationRecord, UserRecord } from "@celluloid/types"; -import * as express from "express"; -import Papa from 'papaparse'; - -import { isProjectOwnerOrCollaborativeMember } from "../auth/Utils"; -import { logger } from "../backends/Logger"; -import * as AnnotationStore from "../store/AnnotationStore"; -import * as CommentStore from "../store/CommentStore"; -import * as ProjectStore from "../store/ProjectStore"; -import { convertToSrt } from "../utils/srt"; -import CommentApi from "./CommentApi"; - - -const log = logger("api/AnnotationApi"); -const js2xmlparser = require("js2xmlparser"); - -const router = express.Router({ mergeParams: true }); - -router.use("/:annotationId/comments", CommentApi); - -function fetchComments(annotation: AnnotationRecord, user: UserRecord) { - return CommentStore.selectByAnnotation(annotation.id, user).then((comments) => - Promise.resolve({ ...annotation, comments } as AnnotationRecord) - ); -} - -router.get('/', (req: express.Request<{ projectId: string }>, res: express.Response) => { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - return ProjectStore.selectOne(projectId, user) - .then(() => AnnotationStore.selectByProject(projectId, user)) - .then((annotations: AnnotationRecord[]) => - Promise.all( - annotations.map((annotation) => fetchComments(annotation, user)) - ) - ) - .then((annotations) => { - return res.status(200).json(annotations); - }) - .catch((error: Error) => { - log.error("Failed to list annotations:", error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.get('/export/:format', async (req: express.Request<{ projectId: string, format: string }>, res: express.Response) => { - const projectId = req.params.projectId; - const format = req.params.format; - const user = req.user as UserRecord; - - const annotations = await AnnotationStore.selectByProject(projectId, user); - const data = await Promise.all( - annotations.map((annotation) => fetchComments(annotation, user)) - ); - - const formated = data.map((annotation) => ({ - startTime: annotation.startTime, - endTime: annotation.stopTime, - text: annotation.text, - comments: annotation.comments.map((comment) => comment.text) - })) - - let content = ""; - if (format === 'xml') { - content = js2xmlparser.parse("annotations", formated); - } else if (format == "csv") { - - content = Papa.unparse(formated); - } else if (format == "srt") { - - content = convertToSrt(formated); - - } - - res.setHeader('Content-Disposition', `attachment; filename="data.${format}"`); - res.setHeader('Content-Type', `text/${format}`); - res.send(content); - -}); - - - -router.post("/", isProjectOwnerOrCollaborativeMember, (req, res) => { - const projectId = req.params.projectId; - const annotation = req.body as AnnotationData; - const user = req.user as UserRecord; - - AnnotationStore.insert(annotation, user, projectId) - .then((result) => fetchComments(result, user)) - .then((result) => { - return res.status(201).json(result); - }) - .catch((error: Error) => { - log.error(error, "Failed to create annotation"); - return res.status(500).send(); - }); -}); - -router.put( - "/:annotationId", - isProjectOwnerOrCollaborativeMember, - (req, res) => { - const annotationId = req.params.annotationId; - const updated = req.body; - const user = req.user as UserRecord; - - AnnotationStore.selectOne(annotationId, user) - .then((old: AnnotationRecord) => - old.userId !== user.id - ? Promise.reject(new Error("UserNotAnnotationOwner")) - : Promise.resolve() - ) - .then(() => AnnotationStore.update(annotationId, updated, user)) - .then((result) => fetchComments(result, user)) - .then((result) => res.status(200).json(result)) - .catch((error: Error) => { - log.error("Failed to update annotation:", error); - if (error.message === "AnnotationNotFound") { - return res.status(404).json({ error: error.message }); - } else if (error.message === "UserNotAnnotationOwner") { - return res.status(403).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); - } -); - -router.delete( - "/:annotationId", - isProjectOwnerOrCollaborativeMember, - (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user as UserRecord; - - AnnotationStore.selectOne(annotationId, user) - .then((old: AnnotationRecord) => - old.userId !== user.id - ? Promise.reject(new Error("UserNotAnnotationOwner")) - : Promise.resolve() - ) - .then(() => AnnotationStore.del(annotationId)) - .then(() => res.status(204).send()) - .catch((error: Error) => { - log.error("Failed to delete annotation:", error); - if (error.message === "AnnotationNotFound") { - return res.status(404).json({ error: error.message }); - } else if (error.message === "UserNotAnnotationOwner") { - return res.status(403).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); - } -); - -export default router; diff --git a/apps/server/src/api/CommentApi.ts b/apps/server/src/api/CommentApi.ts deleted file mode 100644 index 60209f7d..00000000 --- a/apps/server/src/api/CommentApi.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { CommentRecord, UserRecord } from '@celluloid/types'; -import * as express from 'express'; - -import { - isLoggedIn, - isProjectOwnerOrCollaborativeMember -} from '../auth/Utils'; -import { logger } from '../backends/Logger'; -import * as AnnotationStore from '../store/AnnotationStore'; -import * as CommentStore from '../store/CommentStore'; - - -const log = logger('api/CommentApi'); - -const router = express.Router({ mergeParams: true }); - -router.get('/', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user; - - AnnotationStore.selectOne(annotationId, user) - .then(() => CommentStore.selectByAnnotation(annotationId, user as UserRecord)) - .then((comments: CommentRecord[]) => - res.status(200).json(comments)) - .catch((error: Error) => { - log.error('Failed to list comments:', error); - if (error.message === 'AnnotationNotFound') { - res.status(404).json({ error: error.message }); - } else { - res.status(500).send(); - } - }); -}); - -router.post('/', isLoggedIn, isProjectOwnerOrCollaborativeMember, async (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user; - const comment = req.body.text; - - try { - await AnnotationStore.selectOne(annotationId, user); - const result = await CommentStore.insert(annotationId, comment, user as UserRecord); - - log.debug(result, "resutl"); - return res.status(201).json(result) - }catch(error){ - if (error.message === 'AnnotationNotFound') { - return res.status(404).json({ error: error.message }); - } - return res.status(500).send(); - - } -}); - -router.put('/:commentId', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req:any, res) => { - const commentId = req.params.commentId; - const updated = req.body; - const user = req.user; - - CommentStore.selectOne(commentId) - .then((old: CommentRecord) => - old.userId !== user.id - ? Promise.reject(new Error('UserNotCommentOwner')) - : Promise.resolve() - ) - .then(() => CommentStore.update(commentId, updated.text)) - .then(result => res.status(200).json(result)) - .catch((error: Error) => { - log.error('Failed to update comment:', error); - if (error.message === 'CommentNotFound') { - return res.status(404).json({ error: error.message }); - } else if (error.message === 'UserNotCommentOwner') { - return res.status(403).send({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.delete('/:commentId', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req:any, res) => { - const commentId = req.params.commentId; - const user = req.user; - - CommentStore.selectOne(commentId) - .then((comment: CommentRecord) => - comment.userId !== user.id - ? Promise.reject(new Error('UserNotCommentOwner')) - : Promise.resolve() - ) - .then(() => CommentStore.del(commentId)) - .then(() => res.status(204).send()) - .catch((error: Error) => { - log.error('Failed to delete comment:', error); - if (error.message === 'CommentNotFound') { - return res.status(404).json({ error: error.message }); - } else if (error.message === 'UserNotCommentOwner') { - return res.status(403).send({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -export default router; \ No newline at end of file diff --git a/apps/server/src/api/ProjectApi.ts b/apps/server/src/api/ProjectApi.ts deleted file mode 100644 index 16f7a419..00000000 --- a/apps/server/src/api/ProjectApi.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { - ProjectCreateData, - ProjectGraphRecord, - ProjectRecord, - UserRecord, -} from "@celluloid/types"; -import { Router } from "express"; - -import { isProjectOwner, isTeacher } from "../auth/Utils"; -import { logger } from "../backends/Logger"; -import * as ProjectStore from "../store/ProjectStore"; -import AnnotationsApi from "./AnnotationApi"; - -const log = logger("api/ProjectApi"); - -const router = Router({ mergeParams: true }); - -router.use("/:projectId/annotations", AnnotationsApi); - -function fetchMembers( - project: ProjectRecord, - user?: Partial -): Promise { - if (project.collaborative || (user && user.id === project.userId)) { - return ProjectStore.selectProjectMembers(project.id); - } else if (user) { - return ProjectStore.isMember(project.id, user).then((member) => - member ? Promise.resolve([user] as UserRecord[]) : Promise.resolve([]) - ); - } else { - return Promise.resolve([]); - } -} - - - -router.get("/:projectId", (req, res) => { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - ProjectStore.selectOne(projectId, user) - .then((project: any) => { - return res.json(project); - }) - .catch((error: Error) => { - console.error(error) - log.error(`Failed to fetch project ${projectId}:`, error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.post("/", isTeacher, (req, res) => { - const user = req.user as UserRecord; - const project = req.body as ProjectCreateData; - - ProjectStore.insert(project, user) - .then((result: any) => { - return res.status(201).json(result); - }) - .catch((error: Error) => { - console.log(error); - log.error(`Failed to create project: ${JSON.stringify(error)}`); - return res.status(500).send(); - }); -}); - -router.put("/:projectId", isTeacher, isProjectOwner, (req: any, res) => { - ProjectStore.update(req.body, req.params.projectId) - .then((result) => res.status(200).json(result)) - .catch((error) => { - log.error("Failed to update project:", error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId", isTeacher, isProjectOwner, (req, res) => { - ProjectStore.del(req.params.projectId) - .then(() => res.status(204).send()) - .catch((error) => { - log.error("Failed to delete project:", error); - return res.status(500).send(); - }); -}); - -router.get("/:projectId/members", (req, res) => { - const projectId = req.params.projectId; - const user = req.user; - ProjectStore.selectOne(projectId, user as UserRecord) - .then((project: any) => fetchMembers(project, req.user)) - .then((members) => res.status(200).json(members)) - .catch((error) => { - log.error("Failed to list project members:", error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.put("/:projectId/share", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.shareById(projectId, req.body) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to share project with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId/share", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.unshareById(projectId) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to unshare project with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.put("/:projectId/public", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.setPublicById(projectId, true) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to set project public with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId/public", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.setPublicById(projectId, false) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset public on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); -}); - -router.put( - "/:projectId/collaborative", - isTeacher, - isProjectOwner, - (req, res) => { - const projectId = req.params.projectId; - return ProjectStore.setCollaborativeById(projectId, true) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset collaborative on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); - } -); - -router.delete( - "/:projectId/collaborative", - isTeacher, - isProjectOwner, - (req, res) => { - const projectId = req.params.projectId; - return ProjectStore.setCollaborativeById(projectId, false) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset collaborative on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); - } -); - -export default router; diff --git a/apps/server/src/api/TagApi.ts b/apps/server/src/api/TagApi.ts deleted file mode 100644 index 195af3a5..00000000 --- a/apps/server/src/api/TagApi.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as express from 'express'; - -import { isTeacher } from '../auth/Utils'; -import { logger } from '../backends/Logger'; -import * as TagStore from '../store/TagStore'; - -const log = logger('api/Tag'); - -const router = express.Router(); - -router.get('/', (_, res) => { - TagStore.selectAll() - .then(result => res.status(200).json(result)) - .catch(error => { - log.error('Failed to fetch tags:', error); - return res.status(500).send(); - }); -}); - -router.post('/', isTeacher, (req, res) => { - const { name } = req.body; - return TagStore.insert(name) - .then(result => - res.status(201).json(result) - ) - .catch(error => { - log.error('Failed to add new tag:', error); - return res.status(500).send(); - }); -}); - -export default router; diff --git a/apps/server/src/api/UnfurlApi.ts b/apps/server/src/api/UnfurlApi.ts deleted file mode 100644 index cdc08241..00000000 --- a/apps/server/src/api/UnfurlApi.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as express from 'express'; -import { unfurl } from 'unfurl.js' -import { URL } from 'url'; - -import { isLoggedIn } from '../auth/Utils'; -import { logger } from '../backends/Logger'; - -const log = logger('api/UnfulApi'); - -const router = express.Router(); - -type Result = { - faviconUrl: string | undefined - website: string | undefined - imageUrl: string | undefined - title: string | undefined - description: string | undefined -}; - -router.get('/', isLoggedIn, async (req, res) => { - const url = req.query.url as string; - try { - const raw = await unfurl(url); - const parsedUrl = new URL(url as string); - const result: Result = { - faviconUrl: "", - website: "", - imageUrl: undefined, - title: undefined, - description: undefined, - }; - - const ogp = raw.open_graph; - result.website = ogp.url; - - result.title = ogp.title || raw.description; - result.description = ogp.description || raw.description; - result.faviconUrl = raw.favicon - - result.imageUrl = - ogp.images && ogp.images.length > 0 ? - ogp.images[0].url : - undefined; - if (result.title && result.description) { - if (!result.website) { - result.website = parsedUrl.hostname; - } - } - return res.status(200).json(result); - - } catch (e) { - log.error(`could not unfurl link: ${e.message}`); - return res.status(500); - } - - -}); - -export default router; diff --git a/apps/server/src/api/UserApi.ts b/apps/server/src/api/UserApi.ts deleted file mode 100644 index b6939c64..00000000 --- a/apps/server/src/api/UserApi.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import { - validateConfirmResetPassword, - validateConfirmSignup, - validateLogin, - validateSignup, - validateStudentSignup, -} from "@celluloid/validators"; -import { Request, Response, Router } from "express"; -import passport from "passport"; - -import { SigninStrategy } from "../auth/Auth"; -import { - isLoggedIn, - sendConfirmationCode, - sendPasswordReset, -} from "../auth/Utils"; -import { hasConflictedOn } from "../backends/Database"; -import { logger } from "../backends/Logger"; -import * as UserStore from "../store/UserStore"; -import { TeacherServerRecord } from "../types/UserTypes"; - -const log = logger("api/User"); - -const router = Router(); - -router.post("/student-signup", (req, res, next) => { - const payload = req.body; - const result = validateStudentSignup(payload); - - - if (!result.success) { - log.error( - `Failed student signup with data ${payload}: bad request:`, - result - ); - return res.status(400).json(result); - } - return passport.authenticate(SigninStrategy.STUDENT_SIGNUP, (error: Error) => { - if (error) { - console.log("error", error) - log.error( - `Failed student signup with username ${payload.username}:`, - error - ); - if (hasConflictedOn(error, "User", "username")) { - return res.status(409).json({ - success: false, - errors: { username: "UsernameAlreadyTaken" }, - }); - } else if (error.message === "IncorrectProjectPassword") { - return res.status(403).send(); - } else { - return res.status(500).send(); - } - } else { - log.info( - `New signup for student with username ${payload.username}`, - result - ); - return res.status(201).json(result); - } - })(req, res, next); -}); - -router.post("/signup", (req, res, next) => { - const payload = req.body; - const result = validateSignup(payload); - - if (!result.success) { - log.error(`Failed user signup with data ${payload}: bad request:`, result); - return res.status(400).json(result); - } - - return passport.authenticate(SigninStrategy.TEACHER_SIGNUP, (error: Error) => { - if (error) { - log.error(`Failed user signup with email ${payload.email}:`, error); - if (hasConflictedOn(error, "User", "username")) { - return res.status(409).json({ - success: false, - errors: { username: "UsernameAlreadyTaken" }, - }); - } else if (hasConflictedOn(error, "User", "email")) { - return res.status(409).json({ - success: false, - errors: { email: "EmailAlreadyTaken" }, - }); - } else { - return res.status(500).send(); - } - } else { - log.info(`New signup from teacher with email ${payload.email}`, result); - return res.status(201).json(result); - } - })(req, res, next); -}); - -router.post("/login", (req, res, next) => { - const payload = req.body; - const result = validateLogin(req.body); - - if (!result.success) { - log.error( - `Failed user login with data ${JSON.stringify(payload)}: bad request:`, - result - ); - return res.status(400).json(result); - } - return passport.authenticate(SigninStrategy.LOGIN, (error: Error, user: Express.User) => { - if (error) { - log.error(`Failed user login with data ${payload}:`, error); - return res.status(401).json({ - success: false, - errors: { server: error.message }, - }); - } else { - return req.login(user, (err) => { - if (err) { - return res.status(500).send(); - } else { - return res.status(200).json(result); - } - }); - } - })(req, res, next); -}); - -function compareCodes(expected: string, actual: string) { - return expected.replace(/\s/g, "") === actual.replace(/\s/g, ""); -} - -router.post("/confirm-signup", (req, res) => { - const payload: any = req.body; - const result = validateConfirmSignup(payload); - - if (!result.success) { - return res.status(400).json(result); - } - return UserStore.selectOneByUsernameOrEmail(payload.login) - .then((user: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to confirm signup: user` + - ` with email ${payload.login} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - if (compareCodes(user.code || "", payload.code)) { - return UserStore.confirmByEmail(payload.login) - .then(() => res.status(200).json(result)) - .catch((error: Error) => { - log.error( - `Failed to confirm signup for user` + - ` with email ${payload.login}:`, - error - ); - return res.status(500).send(); - }); - } else { - log.error( - `Failed to confirm signup for user with email` + - ` ${payload.login}: received code ${payload.code}, expected ${user.code}` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } - } - }) - .catch((error) => { - log.error(`Failed to confirm signup:`, error); - return res.status(500).send(); - }); -}); - -router.post("/confirm-reset-password", (req, res) => { - const payload: any = req.body; - const result = validateConfirmResetPassword(payload); - - if (!result.success) { - return res.status(400).json(result); - } - return UserStore.selectOneByUsernameOrEmail(payload.login) - .then((user?: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to confirm password reset: user with email ${payload.login} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - if (compareCodes(user.code || "", payload.code)) { - return UserStore.updatePasswordByEmail( - payload.login.trim(), - payload.password - ) - .then(() => res.status(200).json(result)) - .catch((error: Error) => { - log.error( - `Failed to confirm password reset for user with email ${payload.login}`, - error - ); - return res.status(500).send(); - }); - } else { - log.error( - `Failed to confirm password reset for user with email ${payload.login}:` + - ` received code ${payload.code}, expected ${user.code}` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } - } - }) - .catch((error) => { - log.error(`Failed to confirm password reset:`, error); - return res.status(500).send(); - }); -}); - -const resendCode = - (sender: (user: TeacherRecord) => Promise) => - (req: Request, res: Response) => { - const payload = req.body; - - if (!payload.email || payload.email.trim().length === 0) { - return res.status(400).json({ - success: false, - errors: { email: "MissingEmail" }, - }); - } - return UserStore.selectOneByUsernameOrEmail(payload.email) - .then((user?: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to resend authorization code:` + - ` user with email ${payload.email} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - return UserStore.updateCodeByEmail(payload.email).then( - (updatedUser: TeacherRecord) => - sender(updatedUser).then(() => - res.status(200).json({ success: true, errors: {} }) - ) - ); - } - }) - .catch((error: Error) => { - log.error( - `Failed to resend authorization code for user ` + - ` with email ${payload.email}`, - error - ); - return res.status(500).send(); - }); - }; - -router.post("/reset-password", (req, res) => { - return resendCode(sendPasswordReset)(req, res); -}); - -router.post("/resend-code", (req, res) => { - return resendCode(sendConfirmationCode)(req, res); -}); - -router.get("/me", isLoggedIn, (req: any, res) => { - if (req.user) { - return res.status(200).json({ - // compatibility with old frontend - teacher: { - username: req.user.username, - id: req.user.id, - role: req.user.role, - }, - username: req.user.username, - id: req.user.id, - role: req.user.role, - email: req.user.email, - }); - } else { - return res.status(401).send(); - } -}); - -router.put("/logout", isLoggedIn, (req, res) => { - if (req.session) { - req.session.destroy((err) => { - if (err) { - res.status(400).send("Unable to log out"); - } else { - res.send("Logout successful"); - } - }); - } else { - res.end(); - } -}); - -export default router; diff --git a/apps/server/src/api/VideoApi.ts b/apps/server/src/api/VideoApi.ts deleted file mode 100644 index 086bc87a..00000000 --- a/apps/server/src/api/VideoApi.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { PeerTubeVideo } from "@celluloid/types"; -import * as express from "express"; -import fetch from "node-fetch"; -import { last } from "ramda"; -import { URL } from "url"; - -import { logger } from "../backends/Logger"; - -const log = logger("api/videoApi"); - -const router = express.Router(); - -type PeerTubeVideoInfo ={ - id: string; - title: string; - thumbnailUrl: string; - host:string; -} - -async function getPeerVideoInfo(videoUrl: string): Promise { - var parsed = new URL(videoUrl); - - const host = parsed.host; - const videoId = last(parsed.pathname.split("/")); - - const url = `https://${host}/api/v1/videos/${videoId}`; - - try { - const response = await fetch(url, { - method: "GET", - headers: { - Accepts: "application/json", - }, - }); - - if (response.status === 200) { - const data:PeerTubeVideo = await response.json(); - return { - id: data.shortUUID, - host, - title: data.name, - thumbnailUrl: `https://${host}/${data.thumbnailPath}` - }; - } - log.error( - `Could not perform PeerTube API request (error ${response.status})` - ); - throw new Error("Could not perform PeerTube API request "); - } catch (e: any) { - throw new Error("Could not perform PeerTube API request "); - } -} - -router.get("/", async (req, res) => { - if (req.query.url) { - try { - const data = await getPeerVideoInfo(req.query.url as string); - return res.status(200).json(data); - } catch (e: any) { - return res.status(500); - } - } - return res.status(500); -}); - -export default router; diff --git a/apps/server/src/auth/Auth.ts b/apps/server/src/auth/Auth.ts deleted file mode 100644 index 883b6110..00000000 --- a/apps/server/src/auth/Auth.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import bcrypt from 'bcryptjs'; -import passport from "passport"; -import { - Strategy, - VerifyFunction, - VerifyFunctionWithRequest, -} from "passport-local"; - -import { logger } from "../backends/Logger"; -import * as ProjectStore from "../store/ProjectStore"; -import * as UserStore from "../store/UserStore"; -import { TeacherServerRecord, UserServerRecord } from "../types/UserTypes"; -import { sendConfirmationCode } from "./Utils"; - -const log = logger("auth/Auth"); - - -declare global { - namespace Express { - interface User { - id: string; - } - } -} - -export enum SigninStrategy { - LOGIN = "login", - TEACHER_SIGNUP = "teacher-signup", - STUDENT_SIGNUP = "student-signup", -} - -passport.serializeUser((user, done) => { - return Promise.resolve(done(null, user.id)); -}); -passport.deserializeUser((id: string, done: any) => { - return UserStore.selectOne(id) - .then((result: TeacherRecord) => { - if (result) { - return Promise.resolve(done(null, result)); - } else { - log.error( - `Deserialize user failed: user with id` + ` ${id} does not exist` - ); - return Promise.resolve(done(new Error("InvalidUser"))); - } - }) - .catch((error: Error) => Promise.resolve(done(error))); -}); - -const signStudentUp: VerifyFunctionWithRequest = ( - req, - username, - password, - done -) => { - const { shareCode } = req.body; - - return ProjectStore.selectOneByShareName(shareCode) - .then((result) => { - if (result) { - return UserStore.createStudent(username, password, result.id); - return Promise.reject(new Error("IncorrectProjectPassword")); - } - }) - .then((user: any) => Promise.resolve(done(null, user))) - .catch((error: Error) => { - log.error("Failed to signup student:", error); - return Promise.resolve(done(error)); - }); -}; - -const signTeacherUp: VerifyFunctionWithRequest = ( - req, - email, - password, - done -) => { - return UserStore.createTeacher(req.body.username, email, password) - .then((user: TeacherServerRecord) => sendConfirmationCode(user)) - .then((user: TeacherRecord) => Promise.resolve(done(null, user))) - .catch((error: Error) => Promise.resolve(done(error))); -}; - -const logUserIn: VerifyFunction = (login, password, done) => { - return UserStore.selectOneByUsernameOrEmail(login) - .then((user: UserServerRecord) => { - if (!user) { - return Promise.resolve(done(new Error("InvalidUser"))); - } - if (!bcrypt.compareSync(password, user.password)) { - log.error(`Login failed for user ${user.username}: incorrect password`); - return Promise.resolve(done(new Error("InvalidUser"))); - } - if (!user.confirmed && user.role !== "Student") { - log.error(`Login failed: ${user.username} is not confirmed`); - return Promise.resolve(done(new Error("UserNotConfirmed"))); - } - return Promise.resolve(done(null, user)); - }) - .catch((error: Error) => Promise.resolve(done(error))); -}; - -export const loginStrategy = new Strategy( - { usernameField: "login" }, - logUserIn -); - -export const teacherSignupStrategy = new Strategy( - { passReqToCallback: true, usernameField: "email" }, - signTeacherUp -); - -export const studentSignupStrategy = new Strategy( - { passReqToCallback: true, usernameField: "username" }, - signStudentUp -); diff --git a/apps/server/src/auth/Utils.ts b/apps/server/src/auth/Utils.ts deleted file mode 100644 index 8dd7c893..00000000 --- a/apps/server/src/auth/Utils.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { UserRecord } from '@celluloid/types'; -import bcrypt from 'bcryptjs'; -import { paramCase } from 'change-case'; -import { NextFunction, Request, Response } from 'express'; - -import { sendMail } from '../backends/Email'; -import { logger } from '../backends/Logger'; -import * as ProjectStore from '../store/ProjectStore'; -import { TeacherServerRecord } from '../types/UserTypes'; - -const log = logger('auth/Auth'); - -export function hashPassword(password: string) { - const salt = bcrypt.genSaltSync(); - return bcrypt.hashSync(password, salt); -} - -export function isLoggedIn( - req: Request, - res: Response, - next: NextFunction) { - if (!req.user) { - return Promise.resolve(res.status(401).json({ - error: 'LoginRequired' - })); - } - return Promise.resolve(next()); -} - -export function isTeacher( - req: any, - res: Response, - next: NextFunction) { - if ((!req.user || req.user.role !== 'Teacher') && (!req.user || req.user.role !== 'Admin')) { - log.error('User is must be a teacher'); - return Promise.resolve(res.status(403).json({ - error: 'TeacherRoleRequired' - })); - } - return Promise.resolve(next()); -} - -export function isProjectOwner( - req: Request, - res: Response, - next: NextFunction) { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - return ProjectStore.isOwner(projectId, user) - .then((result: boolean) => { - if (result) { - next(); - return Promise.resolve(); - } - log.error('User must be project owner'); - res.status(403).json({ - error: 'ProjectOwnershipRequired' - }); - return Promise.resolve(); - }) - .catch(error => { - log.error('Failed to check project ownership:', error); - return Promise.resolve(res.status(500).send()); - }); -} - -export function isProjectOwnerOrCollaborativeMember( - req: Request, - res: Response, - next: NextFunction) { - - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - ProjectStore.isOwnerOrCollaborativeMember(projectId, user) - .then((result: boolean) => { - if (result) { - next(); - return Promise.resolve(); - } - log.error('User must be project owner or collaborator'); - res.status(403).json({ - error: 'ProjectOwnershipOrMembershipRequired' - }); - return Promise.resolve(); - }) - .catch(error => { - log.error('Failed project ownership/membership test:', error); - return Promise.resolve(res.status(500).send()); - }); -} - -export function generateConfirmationCode() { - const code = () => String(Math.floor(Math.random() * 900) + 100); - const first = code(); - const second = code(); - return `${first}${second}`; -} - -export function generateUniqueShareName(title: string, count: number) { - const compare = (a: string, b: string) => - b.length - a.length; - - const construct = (result: string[], str: string) => { - let res: string[] = [] - if (str) { - if (result.join().length < 6) { - res = [...result, str]; - } - } - return res; - }; - - const prefix = paramCase(title) - .split(/-/) - .sort(compare) - .reduce(construct, []) - .join('-'); - - return `${prefix}${count ? count : ''}`; -} - -export function sendConfirmationCode(user: TeacherServerRecord) { - const subject = `Bienvenue sur Celluloid, ${user.username} !`; - const text = - `Bonjour ${user.username},\n\n` + - `Voici votre code de confirmation : ${user.code}\n\n` + - `Ce code est valable pendant 1 heure.\n\n` + - `Veuillez le saisir dans le formulaire prévu à cet effet.\n\n` + - `L'équipe Celluloid vous souhaite la bienvenue !`; - const html = - `Bonjour ${user.username},` + - `Voici votre code de confirmation : ${user.code}` + - `Ce code est valable pendant 1 heure.` + - `Veuillez le saisir dans le formulaire prévu à cet effet.` + - `L'équipe Celluloid vous souhaite la bienvenue !`; - - return sendMail(user.email, subject, text, html).then(() => - Promise.resolve(user) - ); -} - -export function sendPasswordReset(user: TeacherServerRecord) { - const subject = `${user.username - } : réinitialisation de votre mot de passe Celluloid`; - const text = - `Bonjour ${user.username},\n\n` + - `Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}\n\n` + - `Voici votre code de confirmation: ${user.code}\n\n` + - `Ce code sera valable pendant 1 heure.\n\n` + - `Veuillez le saisir dans le formulaire prévu à cet effet.\n\n` + - `Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.\n\n` + - `Cordialement,\n\n` + - `L'équipe Celluloid`; - const html = - `Bonjour ${user.username},` + - `Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}` + - `Voici votre code de confirmation : ${user.code}` + - `Ce code sera valable pendant 1 heure.` + - `Veuillez le saisir dans le formulaire prévu à cet effet.` + - `Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.` + - `Cordialement,` + - `L'équipe Celluloid`; - - return sendMail(user.email, subject, text, html).then(() => - Promise.resolve(user) - ); -} diff --git a/apps/server/src/backends/Database.ts b/apps/server/src/backends/Database.ts deleted file mode 100644 index bf970f7e..00000000 --- a/apps/server/src/backends/Database.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Knex from "knex"; -import * as R from "ramda"; - -import configuration from "../knexfile" -import { logger } from "./Logger"; - -const log = logger("Database"); - -export const database = Knex(configuration) - - -export const filterNull = - (prop: string) => - // tslint:disable-next-line:no-any - (obj: any) => { - // tslint:disable-next-line:no-any - obj[prop] = obj[prop].filter((elem: any) => elem); - return obj; - }; - -export function getExactlyOne(rows: any[]) { - if (rows.length === 1) { - return Promise.resolve(rows[0]); - } else { - log.error("Update or insert result has less or more than one row", rows); - return Promise.reject(Error("NotExactlyOneRow")); - } -} - -const CONFLICT_ERROR = "23505"; - -interface DatabaseError extends Error { - code?: string; - constraint?: string; -} - -export function hasConflictedOn( - error: DatabaseError, - table: string, - key: string -) { - return ( - error.code && - error.constraint && - error.code === CONFLICT_ERROR && - R.equals(error.constraint.split("_"), [table, key, "key"]) - ); -} diff --git a/apps/server/src/backends/Email.ts b/apps/server/src/backends/Email.ts deleted file mode 100644 index 07068faf..00000000 --- a/apps/server/src/backends/Email.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as mailer from 'nodemailer'; -import smtp from 'nodemailer-smtp-transport'; - -import { logger } from './Logger'; - -const log = logger('Email'); - -const transport = mailer.createTransport(smtp({ - host: process.env.CELLULOID_SMTP_HOST, - port: parseInt(process.env.CELLULOID_SMTP_PORT || "465", 10), - secure: process.env.CELLULOID_SMTP_SECURE === 'true', - -})); - -export function sendMail( - to: string, subject: string, text: string, html: string) { - const mailOptions = { - from: 'Celluloid ', to, subject, text, html - }; - - return new Promise((resolve, reject) => { - transport.sendMail(mailOptions, (error, info) => { - if (error) { - log.error( - `Failed to send email to ${to} with body [${text}]`, error); - // reject(new Error('Email sending failed')); - resolve(null); - } else { - log.info( - `Email sent to ${to} with subject [${subject}]`, info.response); - resolve(null); - } - }); - }); -} diff --git a/apps/server/src/backends/Logger.ts b/apps/server/src/backends/Logger.ts deleted file mode 100644 index 25312d0f..00000000 --- a/apps/server/src/backends/Logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import pino from 'pino'; - -export const Logger = pino({ - level: process.env.CELLULOID_LOG_LEVEL || 'info' -}); - -export const logger = (module: string) => Logger.child({ module }); \ No newline at end of file diff --git a/apps/server/src/database/connection.ts b/apps/server/src/database/connection.ts deleted file mode 100644 index 392269ed..00000000 --- a/apps/server/src/database/connection.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Knex from "knex"; -import configuration from "../knexfile"; -export const knex = Knex(configuration); \ No newline at end of file diff --git a/apps/server/src/http/SessionStore.ts b/apps/server/src/http/SessionStore.ts deleted file mode 100644 index 4038cb12..00000000 --- a/apps/server/src/http/SessionStore.ts +++ /dev/null @@ -1,34 +0,0 @@ -import RedisStore from "connect-redis" -import session from "express-session"; -import { createClient } from "redis"; - -import { logger } from "../backends/Logger"; - -const log = logger("http/Session"); - -export function createSession() { - const redisClient = createClient({ url: process.env.REDIS_URL || "redis://localhost" }); - redisClient.connect().catch((e) => log.error(`redis error : ${e.message}`)); - - const redisStore = new RedisStore({ - client: redisClient, - }) - log.info("redis connected"); - return session({ - store: redisStore, - name: process.env.CELLULOID_COOKIE_NAME - ? process.env.CELLULOID_COOKIE_NAME - : undefined, - cookie: { - domain: process.env.CELLULOID_COOKIE_DOMAIN - ? process.env.CELLULOID_COOKIE_DOMAIN - : undefined, - secure: process.env.CELLULOID_COOKIE_SECURE === "true", - maxAge: 30 * 24 * 3600 * 1000, - httpOnly: true, - }, - secret: process.env.CELLULOID_COOKIE_SECRET as string, - resave: false, - saveUninitialized: true, - }); -} diff --git a/apps/server/src/index.d.ts b/apps/server/src/index.d.ts deleted file mode 100644 index ca88d8ee..00000000 --- a/apps/server/src/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare global { - namespace Express { - interface User { - id: string; - } - - // These open interfaces may be extended in an application-specific manner via declaration merging. - // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts) - interface Request { - user?: { - id?: string; - }; - } - interface Response {} - interface Application {} - } -} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts deleted file mode 100644 index 4834fbe9..00000000 --- a/apps/server/src/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import "./Config"; - -import bodyParser from "body-parser"; -import compression from "compression"; -import express from "express"; -// import expressPino from "express-pino-logger"; -import passport from "passport"; - -import ProjectsApi from "./api/ProjectApi"; -import TagsApi from "./api/TagApi"; -import UnfurlApi from "./api/UnfurlApi"; -import UsersApi from "./api/UserApi"; -import VideosApi from "./api/VideoApi"; -import { - loginStrategy, - SigninStrategy, - studentSignupStrategy, - teacherSignupStrategy, -} from "./auth/Auth"; -import { logger } from "./backends/Logger"; -import { createSession } from "./http/SessionStore"; -const packageJson = require('../package.json'); - -require("cookie-parser"); - -const log = logger("http"); - -passport.use(SigninStrategy.LOGIN, loginStrategy); -passport.use(SigninStrategy.TEACHER_SIGNUP, teacherSignupStrategy); -passport.use(SigninStrategy.STUDENT_SIGNUP, studentSignupStrategy); -const app = express(); -app.enable('trust proxy'); -// app.use(express.static(publicDir)); -// app.use(express.static(clientDir)); -app.use(bodyParser.json()); -app.use(compression()); -app.use(createSession()); -// app.use(expressPino({ logger: log })); -app.use(passport.initialize()); -app.use(passport.session()); - -app.get("/api/status", (_, res) => res.status(200).json({ - commit: process.env.COMMIT, - version: packageJson.version -})); - -app.use("/api/projects", ProjectsApi); -app.use("/api/users", UsersApi); -app.use("/api/tags", TagsApi); -app.use("/api/unfurl", UnfurlApi); -app.use("/api/video", VideosApi); - - -// app.get("/*", (_, res) => res.sendFile(clientApp)); - - -(async () => { - try { - app.listen(process.env.CELLULOID_LISTEN_PORT, () => { - log.info( - `HTTP server listening on port ${process.env.CELLULOID_LISTEN_PORT}` + - ` in ${process.env.NODE_ENV} mode` - ); - }); - } catch (err) { - log.error(err); - } -})(); diff --git a/apps/server/src/knex/index.ts b/apps/server/src/knex/index.ts deleted file mode 100644 index 1976daeb..00000000 --- a/apps/server/src/knex/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from "./types"; - -import { Knex } from "knex"; -import { Annotation, Comment, Project, User } from "./types"; - -declare module "knex/types/tables" { - interface Tables { - // This is same as specifying `knex('users')` - users: User; - annotations: Annotation; - comments: Comment; - projects: Project; - } -} diff --git a/apps/server/src/knex/types.ts b/apps/server/src/knex/types.ts deleted file mode 100644 index 6b0fb885..00000000 --- a/apps/server/src/knex/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -// The TypeScript definitions below are automatically generated. -// Do not touch them, or risk, your modifications being lost. - -export enum UserRole { - Admin = "Admin", - Teacher = "Teacher", - Student = "Student", -} - -export enum Table { - Annotation = "Annotation", - Comment = "Comment", - Language = "Language", - Project = "Project", - Session = "Session", - Tag = "Tag", - TagToProject = "TagToProject", - User = "User", - UserToProject = "UserToProject", -} - -export type Annotation = { - id: string; - text: string; - startTime: number; - stopTime: number; - pause: boolean; - userId: string; - projectId: string; -}; - -export type Comment = { - id: string; - annotationId: string; - userId: string; - text: string; - createdAt: Date; -}; - -export type Language = { - id: string; - name: string | null; -}; - -export type Project = { - id: string; - videoId: string; - title: string; - description: string; - assignments: string[] | null; - publishedAt: Date; - objective: string; - levelStart: number; - levelEnd: number; - public: boolean; - collaborative: boolean; - userId: string; - shared: boolean; - shareName: string | null; - shareExpiresAt: Date | null; - sharePassword: string | null; - host: string; -}; - -export type Session = { - sid: string; - session: string; - expiresAt: Date; -}; - -export type Tag = { - id: string; - name: string; - featured: boolean; -}; - -export type TagToProject = { - tagId: string; - projectId: string; -}; - -export type User = { - id: string; - email: string | null; - password: string; - confirmed: boolean; - code: string | null; - codeGeneratedAt: Date | null; - username: string; - role: UserRole; -}; - -export type UserToProject = { - userId: string; - projectId: string; -}; - diff --git a/apps/server/src/knexfile.ts b/apps/server/src/knexfile.ts deleted file mode 100644 index a351a9fd..00000000 --- a/apps/server/src/knexfile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as dotEnv from 'dotenv'; -import { Knex } from 'knex'; - -dotEnv.config({ path: '../../.env' }); - - -const configuration = { - client: "pg", - connection: { - host: process.env.CELLULOID_PG_HOST, - database: process.env.CELLULOID_PG_DATABASE, - user: process.env.CELLULOID_PG_USER, - password: process.env.CELLULOID_PG_PASSWORD - }, - pool: { - min: 2, - max: 10 - } -} as Knex.Config - -export default configuration; diff --git a/apps/server/src/middleware/installDatabase.ts b/apps/server/src/middleware/installDatabase.ts deleted file mode 100644 index 0922a505..00000000 --- a/apps/server/src/middleware/installDatabase.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import { Express } from "express"; -import Knex from "knex"; -import configuration from "../knexfile"; -export const knex = Knex(configuration); - -export default (app: Express) => { - - app.set("knex", knex); - - // const shutdownActions = getShutdownActions(app); - // shutdownActions.push(() => { - // rootPgPool.end(); - // }); - - }; \ No newline at end of file diff --git a/apps/server/src/migrations/20221107103108_initial.ts b/apps/server/src/migrations/20221107103108_initial.ts deleted file mode 100644 index 7dc38604..00000000 --- a/apps/server/src/migrations/20221107103108_initial.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.raw('CREATE EXTENSION IF NOT EXISTS "pgcrypto"'); - await knex.raw('CREATE EXTENSION IF NOT EXISTS "plpgsql"'); - await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); - - if (!(await knex.schema.hasTable("User"))) { - await knex.schema.createTable("User", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.string("email").notNullable().unique(); - table.string("username").notNullable().unique(); - table.string("password").notNullable(); - table.boolean("confirmed").notNullable().defaultTo(false); - table.text("code"); - table.timestamp("codeGeneratedAt"); - table - .enu("role", ["Admin", "Teacher", "Student"], { - useNative: true, - enumName: "UserRole", - }) - .checkIn(["Teacher", "Admin"]); - table.jsonb("extra").defaultTo({}); - }); - - knex.schema.raw(` - ALTER TABLE - User - ADD CONSTRAINT - Project_check_userValid - CHECK - ((((role = ANY (ARRAY['Teacher'::public."UserRole", 'Admin'::public."UserRole"])) AND (email IS NOT NULL)) OR ((role = 'Student'::public."UserRole")))) - `); - } - - await knex.schema.createTable("Language", (table) => { - table.text("id").notNullable().unique(); - table.text("name").notNullable(); - }); - - await knex.schema.createTable("Project", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("videoId").notNullable(); - - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - - table.text("title").notNullable(); - table.text("description").notNullable(); - table.text("host"); - table.specificType("assignments", "text[]"); - table.timestamp("publishedAt").notNullable().defaultTo(knex.fn.now()); - table.text("objective").notNullable(); - table.integer("levelStart").notNullable(); - table.integer("levelEnd").notNullable(); - table.boolean("public").notNullable().defaultTo(false); - table.boolean("collaborative").notNullable(); - table.boolean("shared").notNullable().defaultTo(false); - table.text("shareName").unique(); - table.timestamp("shareExpiresAt"), table.text("sharePassword"); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Annotation", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - - table.text("text").notNullable(); - table.float("startTime").notNullable(); - table.float("stopTime").notNullable(); - table.boolean("pause").notNullable(); - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - table - .uuid("projectId") - .notNullable() - .references("Project.id") - .onDelete("CASCADE"); - table.timestamp("createdAt").defaultTo(knex.fn.now()); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Comment", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("text").notNullable(); - table - .uuid("annotationId") - .notNullable() - .references("Annotation.id") - .onDelete("CASCADE"); - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - table.timestamp("createdAt").defaultTo(knex.fn.now()); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Tag", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("name").notNullable(); - table.boolean("featured").notNullable().defaultTo(false); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("TagToProject", (table) => { - table.uuid("tagId").notNullable().references("Tag.id").onDelete("CASCADE"); - table - .uuid("projectId") - .notNullable() - .references("Project.id") - .onDelete("CASCADE"); - table.unique(["tagId", "projectId"], { - indexName: "TagToProjectTagIdProjectIdUnique", - }); - }); - - await knex.schema.createTable("UserToProject", (table) => { - table.uuid("userId").references("User.id").onDelete("CASCADE"); - table.uuid("projectId").references("Project.id").onDelete("CASCADE"); - }); - // } -} - -exports.down = function (knex: Knex): Promise { - throw new Error("Enable to rollback, please use backup"); -}; diff --git a/apps/server/src/migrations/20230117091650_fix-role.ts b/apps/server/src/migrations/20230117091650_fix-role.ts deleted file mode 100644 index 14197715..00000000 --- a/apps/server/src/migrations/20230117091650_fix-role.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.raw( - `ALTER TABLE public."User" DROP CONSTRAINT IF EXISTS "User_role_check"; - ALTER TABLE public."User" ALTER COLUMN email DROP NOT NULL;` - ); -} - -export async function down(): Promise { - return; -} diff --git a/apps/server/src/seeds/default_tags.ts b/apps/server/src/seeds/default_tags.ts deleted file mode 100644 index 4b23c4a1..00000000 --- a/apps/server/src/seeds/default_tags.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Knex } from "knex"; - -const records = [ - { - id: "58d5d8e2-63fd-4f71-993a-642537afe905", - name: "Français - Lettres", - featured: true, - }, - { - id: "b00273d0-f65e-4115-807b-4d6eff189b43", - name: "Mathématiques", - featured: true, - }, - { - id: "f908ead8-f15c-4f5f-84c3-4dfb7e31f1d3", - name: "Histoire", - featured: true, - }, - { - id: "05639d30-37e5-4280-8bcb-092f39c28819", - name: "Géographie", - featured: true, - }, - { - id: "2dc18987-a44f-4b5f-9c0d-be81042b767b", - name: "Technologie", - featured: true, - }, - { - id: "c64c3545-096d-4cb6-9df8-4600aac715bc", - name: "Ëducation civique", - featured: true, - }, - { - id: "553f4da0-5f1d-4ec0-aafb-7dc8adb109e6", - name: "Sciences Physiques", - featured: true, - }, - { - id: "45cf959a-69c5-451a-b185-ef16f2344d7d", - name: "Sport", - featured: true, - }, - { - id: "27eba991-e805-4a82-8281-618b1236380d", - name: "Sciences de la Vie", - featured: true, - }, - { - id: "5a93968a-9047-4d80-a601-539e8393a4cb", - name: "Langues", - featured: true, - }, - { - id: "67b3121e-6893-4ed3-b2df-5318e9bfda5c", - name: "Musique", - featured: true, - }, - { - id: "fbd709d6-68ff-4540-800e-8649bec88892", - name: "Arts", - featured: true, - }, - { - id: "4fa36e4b-b9ea-42ee-8c7a-cf27fa7292eb", - name: "Economie", - featured: true, - }, - { - id: "fefba9b8-32c4-41a7-a3ec-1d810b42d843", - name: "Philosophie", - featured: true, - }, - { - id: "e61332fa-44e5-4719-9e8a-3a62848c44dd", - name: "Projets de recherche", - featured: true, - }, -]; - -export async function seed(knex: Knex): Promise { - await knex.transaction((trx) => { - return trx("Tag").insert(records).onConflict("id").merge(["name", "featured"]) - }); -} diff --git a/apps/server/src/store/AnnotationStore.ts b/apps/server/src/store/AnnotationStore.ts deleted file mode 100644 index 131277da..00000000 --- a/apps/server/src/store/AnnotationStore.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { AnnotationData, AnnotationRecord, UserRecord } from "@celluloid/types"; -import { Knex } from "knex"; - -import { database, getExactlyOne } from "../backends/Database"; -import * as ProjectStore from "./ProjectStore"; - -export function selectByProject(projectId: string, user?: UserRecord) { - return database - .select( - database.raw('"Annotation".*'), - database.raw( - "json_build_object(" + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"' - ) - ) - .from("Annotation") - .innerJoin("Project", "Project.id", "Annotation.projectId") - .innerJoin("User", "User.id", "Annotation.userId") - .where("Annotation.projectId", projectId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .orderBy("Annotation.startTime", "asc"); -} - -export function selectOne( - annotation: string | { id: string }, - user?: Partial -) { - let annotationId = annotation; - - if (typeof annotation === "object") { - annotationId = annotation.id; - } - - return database - .select( - database.raw('"Annotation".*'), - database.raw( - "json_build_object(" + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"' - ) - ) - .from("Annotation") - .innerJoin("Project", "Project.id", "Annotation.projectId") - .innerJoin("User", "User.id", "Annotation.userId") - .where("Annotation.id", annotationId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .first() - .then((row?: AnnotationRecord) => - row - ? Promise.resolve(row) - : Promise.reject(new Error("AnnotationNotFound")) - ); -} - -export function insert( - annotation: AnnotationData, - user: UserRecord, - projectId: string -) { - return database("Annotation") - .insert({ - text: annotation.text, - startTime: annotation.startTime, - stopTime: annotation.stopTime, - pause: annotation.pause, - userId: user.id, - projectId: projectId, - }) - .returning("id") - .then(getExactlyOne) - .then((id) => selectOne(id, user)); -} - -export function update(id: string, data: AnnotationData, user: UserRecord) { - return database("Annotation") - .update({ - text: data.text, - startTime: data.startTime, - stopTime: data.stopTime, - pause: data.pause, - }) - .returning("id") - .where("id", id) - .then(getExactlyOne) - .then(() => selectOne(id, user)); -} - -export function del(id: string) { - return database("Annotation").where("id", id).del(); -} diff --git a/apps/server/src/store/CommentStore.ts b/apps/server/src/store/CommentStore.ts deleted file mode 100644 index a57b0901..00000000 --- a/apps/server/src/store/CommentStore.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { CommentRecord, UserRecord } from '@celluloid/types'; -import { Knex } from 'knex'; - -import { database, getExactlyOne } from '../backends/Database'; -import { Logger } from '../backends/Logger'; -import * as ProjectStore from './ProjectStore'; - -export function selectByAnnotation(annotationId: string, user: Partial) { - return database.select( - database.raw('"Comment".*'), - database.raw( - 'json_build_object(' + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"')) - .from('Comment') - .innerJoin('Annotation', 'Annotation.id', 'Comment.annotationId') - .innerJoin('User', 'User.id', 'Comment.userId') - .innerJoin('Project', 'Project.id', 'Annotation.projectId') - .where('Comment.annotationId', annotationId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.where('User.id', user.id); - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .orderBy('Comment.createdAt', 'asc'); -} - -export function selectOne(commentId: string) { - - console.log(commentId, "selectOne") - return database.select( - database.raw('"Comment".*'), - database.raw( - 'json_build_object(' + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"')) - .from('Comment') - .innerJoin('User', 'User.id', 'Comment.userId') - .where('Comment.id', commentId) - .first() - .then((row?: CommentRecord) => row ? Promise.resolve(row) : - Promise.reject(new Error('CommentNotFound'))); -} - -export function insert(annotationId: string, text: string, user: Partial) { - return database('Comment') - .insert({ - annotationId, - userId: user.id, - text, - createdAt: database.raw('NOW()') - }) - .returning('id') - .then(getExactlyOne) - .then(row => selectOne(row.id)); -} - -export function update(id: string, text: string) { - return database('Comment') - .update({ - text - }) - .where('id', id) - .returning('id') - .then(getExactlyOne) - .then(() => selectOne(id)); -} - -export function del(id: string) { - return database('Comment') - .where('id', id) - .del(); -} \ No newline at end of file diff --git a/apps/server/src/store/ProjectStore.ts b/apps/server/src/store/ProjectStore.ts deleted file mode 100644 index b01082d4..00000000 --- a/apps/server/src/store/ProjectStore.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - ProjectCreateData, - ProjectRecord, - ProjectShareData, - UserRecord, -} from "@celluloid/types"; -import { Knex } from "knex"; - -import { generateUniqueShareName } from "../auth/Utils"; -import { - database, - filterNull, - getExactlyOne, - hasConflictedOn, -} from "../backends/Database"; -import { logger } from "../backends/Logger"; -import { Project, User } from "../knex"; -import { tagProject } from "./TagStore"; - -const log = logger("store/ProjectStore"); - -export const orIsMember = (nested: Knex.QueryBuilder, user?: UserRecord) => - user - ? nested.orWhereIn( - "Project.id", - database - .select("projectId") - .from("UserToProject") - .where("userId", user.id) - ) - : nested; - -export const orIsOwner = (nested: Knex.QueryBuilder, user?: UserRecord) => - user ? nested.orWhere("Project.userId", user.id) : nested; - -function filterUserProps({ id, username, role }: UserRecord) { - return { - id, - username, - role, - }; -} - -export function isOwnerOrCollaborativeMember( - projectId: string, - user: UserRecord -) { - return Promise.all([ - isOwner(projectId, user), - isCollaborativeMember(projectId, user), - ]).then(([owner, member]: boolean[]) => owner || member); -} - -export function isOwner(projectId: string, user: UserRecord) { - return database - .first("id") - .from("Project") - .where("id", projectId) - .andWhere("userId", user.id) - .then((row: string) => (row ? true : false)); -} - -export function isMember(projectId: string, user: Partial) { - return ( - database - .first("projectId") - .from("UserToProject") - .where("UserToProject.projectId", projectId) - // @ts-ignore - .andWhere("UserToProject.userId", user.id) - .then((row: string) => (row ? true : false)) - ); -} - -export function isCollaborativeMember(projectId: string, user: UserRecord) { - return database - .first("projectId") - .from("UserToProject") - .innerJoin("Project", "Project.id", "UserToProject.projectId") - .where("UserToProject.projectId", projectId) - .andWhere("UserToProject.userId", user.id) - .andWhere("Project.collaborative", true) - .then((row: string) => (row ? true : false)); -} - -// < Project[] &{ -// tags: Tag[], -// user: User -// }> -export function selectAll(user: UserRecord): Promise { - return database("projects") - .select( - database.raw('"Project".*'), - // database.raw(`to_json(array_agg("Tag")) AS "tags"`), - database.raw(`row_to_json("User") as "user"`) - ) - .from("Project") - .innerJoin("User", "User.id", "Project.userId") - // .leftJoin("TagToProject", "Project.id", "TagToProject.projectId") - // .leftJoin("Tag", "Tag.id", "TagToProject.tagId") - .where("Project.public", true) - .modify(orIsOwner, user) - .modify(orIsMember, user) - .groupBy("Project.id", "User.id") - .then((rows) => - rows.map((r: any) => ({ - ...r, - user: filterUserProps(r.user), - }) - ) - ); -} - - - -export function selectOneByShareName(shareCode: string) { - return database.first("*").from("Project").where("shareCode", shareCode); -} - -export function selectOne(projectId: string, user: Partial) { - return database - .first( - database.raw('"Project".*'), - // database.raw(`to_json(array_agg("Tag")) as "tags"`), - database.raw(`row_to_json("User") as "user"`) - ) - .from("Project") - .innerJoin("User", "User.id", "Project.userId") - // .leftJoin("TagToProject", "Project.id", "TagToProject.projectId") - // .leftJoin("Tag", "Tag.id", "TagToProject.tagId") - .where((nested: Knex.QueryBuilder) => { - nested.where("Project.public", true); - nested.modify(orIsMember, user); - nested.modify(orIsOwner, user); - }) - .andWhere("Project.id", projectId) - .groupBy("Project.id", "User.id") - .then((row?) => { - return new Promise((resolve, reject) => { - if (row) { - return selectProjectMembers(projectId).then((members) => - resolve( - { - user: filterUserProps(row.user), - members, - ...row, - } - ) - ); - } else { - return reject(new Error("ProjectNotFound")); - } - }); - }); -} - -export function insert(project: ProjectCreateData, user: UserRecord) { - const INSERT_RETRY_COUNT = 20; - const { tags, ...props } = project; - const query: any = (retry: number) => - database("Project") - .insert({ - ...props, - userId: user.id, - publishedAt: database.raw("NOW()"), - shareName: generateUniqueShareName(props.title, retry), - }) - .returning("*") - .then(getExactlyOne) - .catch((error) => { - if (hasConflictedOn(error, "User", "username")) { - if (retry < INSERT_RETRY_COUNT) { - return query(retry + 1); - } else { - log.warn( - "Failed to insert project: unique share name generation failed" - ); - } - } - throw error; - }); - return query(0).then((record: any) => - Promise.all(project.tags.map((tag) => tagProject(tag.id, record.id))).then( - () => Promise.resolve({ tags, ...record }) - ) - ); -} - -export function update(projectId: string, props: ProjectRecord) { - return database("Project") - .update(props) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function del(projectId: string) { - return database("Project").where("id", projectId).del(); -} - -export function shareById(projectId: string, data: ProjectShareData) { - return database("Project") - .update({ - shared: true, - sharePassword: data.sharePassword, - }) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function unshareById(projectId: string) { - return database("Project") - .update({ - shared: false, - sharePassword: null, - }) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function selectProjectMembers(projectId: string) { - return database - .select("User.id", "User.username", "User.role") - .from("UserToProject") - .innerJoin("User", "User.id", "UserToProject.userId") - .where("UserToProject.projectId", projectId) - .then((rows) => rows.map(filterUserProps)); -} - -export function setPublicById(projectId: string, _public: boolean) { - return database("Project") - .update({ - public: _public, - }) - .where("id", projectId) - .returning("*") - .then(getExactlyOne); -} - -export function setCollaborativeById( - projectId: string, - collaborative: boolean -) { - return database("Project") - .update({ - collaborative, - }) - .where("id", projectId) - .returning("*") - .then(getExactlyOne); -} - diff --git a/apps/server/src/store/TagStore.ts b/apps/server/src/store/TagStore.ts deleted file mode 100644 index 5cca7bba..00000000 --- a/apps/server/src/store/TagStore.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { database, getExactlyOne } from '../backends/Database'; - -export function selectAll() { - return database.select() - .from('Tag'); -} - -export function insert(name: string) { - return database('Tag') - .insert({ - 'name': name, - 'featured': false - }) - .returning('*') - .then(getExactlyOne); -} - -export function tagProject(tagId: string, projectId: string) { - return database('TagToProject') - .insert({ - tagId, - projectId - }); -} - -export function untagProject(tagId: string, projectId: string) { - return database('TagToProject') - .insert({ - tagId, - projectId - }); -} \ No newline at end of file diff --git a/apps/server/src/store/UserStore.ts b/apps/server/src/store/UserStore.ts deleted file mode 100644 index ed2c0d11..00000000 --- a/apps/server/src/store/UserStore.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { UserRecord } from "@celluloid/types"; -import { Knex } from "knex"; - -import { generateConfirmationCode, hashPassword } from "../auth/Utils"; -import { database, getExactlyOne } from "../backends/Database"; - -export function createStudent( - username: string, - password: string, - projectId: string -) { - - - return database.transaction((transaction) => - database("User") - .transacting(transaction) - .insert({ - password: hashPassword(password), - username, - confirmed: false, - role: "Student", - }) - .returning("*") - .then(getExactlyOne) - .then((student) => - joinProject(student.id, projectId, transaction).then(() => - Promise.resolve(student) - ) - ) - .then(transaction.commit) - .catch(transaction.rollback) - ); -} - -export function createTeacher( - username: string, - email: string, - password: string -) { - return database("User") - .insert({ - email, - password: hashPassword(password), - username, - code: generateConfirmationCode(), - codeGeneratedAt: database.raw("NOW()"), - confirmed: false, - role: "Teacher", - }) - .returning("*") - .then(getExactlyOne); -} - -export function updatePasswordByEmail(login: string, password: string) { - return database("User") - .update({ - password: hashPassword(password), - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function updateCodeByEmail(login: string) { - return database("User") - .update({ - code: generateConfirmationCode(), - codeGeneratedAt: database.raw("NOW()"), - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function confirmByEmail(login: string) { - return database("User") - .update({ - code: null, - codeGeneratedAt: null, - confirmed: true, - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function selectOne(id: string) { - return database("User").first().where("id", id); -} - -export function selectOneByUsernameOrEmail(login: string) { - return database("User") - .first() - .where("username", login) - .orWhere("email", login); -} - -function withTransaction( - query: Knex.QueryBuilder, - transaction?: Knex.Transaction -) { - return transaction ? query.transacting(transaction) : query; -} - -export function joinProject( - userId: string, - projectId: string, - transaction?: Knex.Transaction -) { - return withTransaction(database("UserToProject"), transaction).insert({ - userId, - projectId, - }); -} - -export function leaveProject(userId: string, projectId: string) { - return database("UserToProject") - .where("userId", userId) - .andWhere("projectId", projectId) - .del(); -} diff --git a/apps/server/src/types/UserTypes.ts b/apps/server/src/types/UserTypes.ts deleted file mode 100644 index 015754ec..00000000 --- a/apps/server/src/types/UserTypes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - UserRecord, -} from '@celluloid/types'; - -export interface UserServerRecord extends UserRecord { - confirmed: boolean; - password: string; -} - -export interface TeacherServerRecord extends UserServerRecord { - code?: string; - codeExpiresAt?: Date; - email: string; -} - -export interface AdminServerRecord extends UserServerRecord { - code: string; - codeExpiresAt: Date; - email: string; -} diff --git a/apps/server/src/utils/generate-types.ts b/apps/server/src/utils/generate-types.ts deleted file mode 100644 index ff1b8942..00000000 --- a/apps/server/src/utils/generate-types.ts +++ /dev/null @@ -1,9 +0,0 @@ -const { knex } = require("knex"); -const { updateTypes } = require("knex-types"); - -const db = knex(require("../knexfile").development); - -updateTypes(db, { output: "../types.ts" }).catch((err:any) => { - console.error(err); - process.exit(1); -}); \ No newline at end of file diff --git a/apps/server/src/utils/srt.ts b/apps/server/src/utils/srt.ts deleted file mode 100644 index 4e2f1b35..00000000 --- a/apps/server/src/utils/srt.ts +++ /dev/null @@ -1,35 +0,0 @@ -interface Subtitle { - startTime: number; - endTime: number; - text: string; -} - -export function convertToSrt(json: Subtitle[]): string { - let srt = ''; - - json.forEach((subtitle: Subtitle, index: number) => { - const { startTime, endTime, text } = subtitle; - - // Format the start and end time in HH:MM:SS,mmm format - const formattedStartTime = formatTime(startTime); - const formattedEndTime = formatTime(endTime); - - // Add the subtitle index, start and end time, and text to the SRT format - srt += `${index + 1}\n${formattedStartTime} --> ${formattedEndTime}\n${text}\n\n`; - }); - - return srt; -} - -function formatTime(time: number): string { - const hours = Math.floor(time / 3600); - const minutes = Math.floor((time % 3600) / 60); - const seconds = Math.floor(time % 60); - const milliseconds = Math.round((time % 1) * 1000); - - return `${padNumber(hours)}:${padNumber(minutes)}:${padNumber(seconds)},${padNumber(milliseconds, 3)}`; -} - -function padNumber(number: number, length = 2): string { - return number.toString().padStart(length, '0'); -} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json deleted file mode 100644 index 5e89adc9..00000000 --- a/apps/server/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "@celluloid/config/tsconfig/node16.json", - "compilerOptions": { - "paths": { - /* IMPORTANT: this must be the same of 'src/aliases.ts' */ - "~/*": [ - "./*" - ] - }, - }, - "include": [ - "**/*.ts", - "**/*.tsx", - "tsup.config.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/apps/server/tsup.config.ts b/apps/server/tsup.config.ts deleted file mode 100644 index 11a619b2..00000000 --- a/apps/server/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig({ - clean: true, - dts: false, - entry: ["src/index.ts"], - format: ["cjs"], - minify: isProduction, - sourcemap: true, -}); diff --git a/package.json b/package.json index 4d2bcad9..ff5e8256 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "prettier:all": "prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,md}\"", "--shortcuts to run commands in workspaces--": "", "frontend": "yarn workspace frontend", - "server": "yarn workspace server", "admin": "yarn workspace admin", "backend": "yarn workspace backend", "prisma": "yarn workspace @celluloid/prisma", diff --git a/packages/passport/src/index.ts b/packages/passport/src/index.ts index 45101d08..928d38d2 100644 --- a/packages/passport/src/index.ts +++ b/packages/passport/src/index.ts @@ -2,5 +2,6 @@ export * from "./errors"; export { createSession } from "./session"; import passport from './passport'; +export * from "./passport"; export { passport }; diff --git a/packages/passport/src/passport.ts b/packages/passport/src/passport.ts index 97ff6253..32209d47 100644 --- a/packages/passport/src/passport.ts +++ b/packages/passport/src/passport.ts @@ -46,27 +46,25 @@ passport.use( ); -const loginStrategy = new LocalStrategy( - { usernameField: "login" }, - async (login, password, done) => { +const loginStrategy = new LocalStrategy(async (login, password, done) => { - const user = await prisma.user.findFirst({ - where: { - OR: [{ email: login }, { username: login, }] - } - }); - - if (!user) { - return Promise.resolve(done(new InvalidUserError("User not found"))); - } - if (!bcrypt.compareSync(password, user.password)) { - return Promise.resolve(done(new InvalidUserError(`Login failed for user ${user.username}: incorrect password`))); + const user = await prisma.user.findFirst({ + where: { + OR: [{ email: login }, { username: login, }] } - if (!user.confirmed && user.role !== UserRole.Student) { - return Promise.resolve(done(new UserNotConfirmed(`Login failed: ${user.username} is not confirmed`))); - } - return Promise.resolve(done(null, user)); + }); + + if (!user) { + return done(new InvalidUserError("User not found")); + } + if (!bcrypt.compareSync(password, user.password)) { + return done(new InvalidUserError(`Login failed for user ${user.username}: incorrect password`)); } + if (!user.confirmed && user.role !== UserRole.Student) { + return done(new UserNotConfirmed(`Login failed: ${user.username} is not confirmed`)); + } + return done(null, user); +} ); passport.use(SigninStrategy.LOGIN, loginStrategy); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index c79991fb..447fe1c4 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -24,6 +24,7 @@ "dev": "tsup --watch" }, "dependencies": { + "@celluloid/passport": "*", "@celluloid/prisma": "*", "@trpc/server": "^10.40.0", "bcryptjs": "^2.4.3", @@ -33,6 +34,9 @@ "express-session": "^1.17.3", "js2xmlparser": "^5.0.0", "lodash": "^4.17.21", + "mjml": "^4.14.1", + "nodemailer": "^6.9.7", + "nodemailer-smtp-transport": "^2.7.4", "papaparse": "^5.4.1", "trpc-openapi": "^1.2.0", "uuid": "^9.0.1", @@ -41,6 +45,7 @@ "devDependencies": { "@celluloid/config": "*", "@types/express-session": "^1.17.8", + "@types/mjml": "^4.7.3", "@types/uuid": "^9.0.4", "tsup": "^7.2.0" } diff --git a/packages/trpc/src/mailer/sendMail.ts b/packages/trpc/src/mailer/sendMail.ts new file mode 100644 index 00000000..2dea4e91 --- /dev/null +++ b/packages/trpc/src/mailer/sendMail.ts @@ -0,0 +1,93 @@ +import mjml2html from 'mjml'; +import * as nodemailer from "nodemailer"; + +import getTransport from "./transport"; + +const EMAIL_FROM = process.env.EMAIL_FROM || "no-reply@celluloid.huma-num.fr"; + +const isDev = process.env.NODE_ENV !== "production"; + + +export async function sendMail( + to: string, subject: string, html: string) { + const transport = await getTransport(); + const mailOptions = { + from: `Celluloid <${EMAIL_FROM}>`, to, subject, html + }; + + const info = await transport.sendMail(mailOptions); + + if (isDev) { + const url = nodemailer.getTestMessageUrl(info); + if (url) { + // Hex codes here equivalent to chalk.blue.underline + console.log( + `Development email preview: \x1B[34m\x1B[4m${url}\x1B[24m\x1B[39m` + ); + } + } + +} + + +export async function sendPasswordReset({ username, email, code }: { username: string, email: string, code: string }) { + const subject = `${username} : réinitialisation de votre mot de passe Celluloid`; + + const mjmlTemplate = ` + + + + + Bonjour ${username}, + Nous avons reçu une demande de réinitialisation de mot de passe pour l'adresse email ${email} + Voici votre code de confirmation : ${code} + Ce code sera valable pendant 1 heure. + Veuillez le saisir dans le formulaire prévu à cet effet. + Si vous n'êtes pas à l'origine de cette demande, veuillez simplement ignorer ce mail. + Cordialement, + L'équipe Celluloid + + + + + `; + + const { html } = mjml2html(mjmlTemplate); + await sendMail(email, subject, html) +} + + + +export async function sendConfirmationCode({ username, email, code }: { username: string, email: string, code: string }) { + const subject = `${username} : réinitialisation de votre mot de passe Celluloid`; + + const mjmlTemplate = ` + + + + + + Bonjour ${username}, + + + Voici votre code de confirmation : ${code} + + + Ce code est valable pendant 1 heure. + + + Veuillez le saisir dans le formulaire prévu à cet effet. + + + L'équipe Celluloid vous souhaite la bienvenue ! + + + + + + `; + + const { html } = mjml2html(mjmlTemplate); + await sendMail(email, subject, html) +} + diff --git a/packages/trpc/src/mailer/transport.ts b/packages/trpc/src/mailer/transport.ts new file mode 100644 index 00000000..45a56c12 --- /dev/null +++ b/packages/trpc/src/mailer/transport.ts @@ -0,0 +1,72 @@ +import { promises as fsp } from "fs"; +import * as nodemailer from "nodemailer"; + +const { readFile, writeFile } = fsp; + +const isTest = process.env.NODE_ENV === "test"; +const isDev = process.env.NODE_ENV !== "production"; + +let transporterPromise: Promise; +const etherealFilename = `${process.cwd()}/.ethereal`; + +let logged = false; + +export default function getTransport(): Promise { + if (!transporterPromise) { + transporterPromise = (async () => { + if (isTest) { + return nodemailer.createTransport({ + jsonTransport: true, + }); + } else if (isDev) { + let account; + try { + const testAccountJson = await readFile(etherealFilename, "utf8"); + account = JSON.parse(testAccountJson); + } catch (e: any) { + account = await nodemailer.createTestAccount(); + await writeFile(etherealFilename, JSON.stringify(account)); + } + if (!logged) { + logged = true; + console.log(); + console.log(); + console.log( + // Escapes equivalent to chalk.bold + "\x1B[1m" + + " ✉️ Emails in development are sent via ethereal.email; your credentials follow:" + + "\x1B[22m" + ); + console.log(" Site: https://ethereal.email/login"); + console.log(` Username: ${account.user}`); + console.log(` Password: ${account.pass}`); + console.log(); + console.log(); + } + return nodemailer.createTransport({ + host: "smtp.ethereal.email", + port: 587, + secure: false, + auth: { + user: account.user, + pass: account.pass, + }, + }); + } else { + if (!process.env.SMTP_HOST) { + throw new Error("Misconfiguration: no SMTP_HOST"); + } + if (!process.env.SMTP_PORT) { + throw new Error("Misconfiguration: no SMTP_PORT"); + } + return nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || "465", 10), + secure: process.env.SMTP_SECURE === 'true', + }); + } + })(); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return transporterPromise!; +} diff --git a/packages/trpc/src/routers/user.ts b/packages/trpc/src/routers/user.ts index db57cee6..08db383f 100644 --- a/packages/trpc/src/routers/user.ts +++ b/packages/trpc/src/routers/user.ts @@ -1,30 +1,381 @@ -import { prisma } from "@celluloid/prisma" +import { passport, SigninStrategy } from "@celluloid/passport"; +import { Prisma, prisma, UserRole } from "@celluloid/prisma" import { TRPCError } from "@trpc/server"; import { z } from 'zod'; +import { sendConfirmationCode, sendPasswordReset } from "../mailer/sendMail"; import { protectedProcedure, publicProcedure, router } from '../trpc'; +import { compareCodes, generateOtp, hashPassword } from "../utils/forgot"; + +export const defaultUserSelect = Prisma.validator()({ + id: true, + username: true, + role: true, + initial: true, + color: true, +}); export const UserSchema = z.object({ - id: z.string(), - username: z.string(), - role: z.string(), - initial: z.string(), - color: z.string(), + id: z.string({ description: 'The unique identifier for the user' }), + username: z.string({ description: 'The username for the user' }), + role: z.nativeEnum(UserRole, { description: 'The role assigned to the user, either Admin or User' }).nullable(), + initial: z.string({ description: 'The initial letter or string for user representation' }), + color: z.string({ description: 'The color code associated with the user' }) }); export const userRouter = router({ - login: publicProcedure.input( + login: publicProcedure + .meta({ + openapi: { + method: 'POST', + path: '/login', + description: 'This endpoint allows a user to login.' + } + }) + .input( + z.object({ + username: z.string({ description: 'The username of the user' }), + password: z.string({ description: 'The password for the user' }) + }), + ).output(UserSchema.nullable()) + .mutation(async ({ ctx, input }) => { + + ctx.req.body = input; + + await new Promise((resolve, reject) => { + passport.authenticate(SigninStrategy.LOGIN, { + failWithError: true + })(ctx.req, ctx.res, (err: Error, user: Express.User) => { + if (err) return reject(err); + resolve(user); + }) + }).catch(err => { + console.log(err.name); + + if (err?.name === 'AuthenticationError') { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Incorrect username or password.' + }) + } else if (err?.name === "UserNotConfirmed") { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'UserNotConfirmed' + }) + } + + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: err + }) + }) + + const user = await prisma.user.findFirst({ + select: defaultUserSelect, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } + }); + + return user; + }), + forgot: publicProcedure.input( + z.object({ + email: z.string() + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true }, + where: { + OR: [{ email: input.email }, { username: input.email, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + if (!user.email) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `User account doesn't have email address`, + }); + } + + const otp = generateOtp(); + await prisma.user.update({ + where: { id: user.id }, + data: { + code: otp, + codeGeneratedAt: new Date() + } + }) + await sendPasswordReset({ email: user.email, username: user.username, code: otp }) + return { status: true } + }), + + recover: publicProcedure.input( + z.object({ + username: z.string(), + code: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + + if (!compareCodes(input.code, user.code || "")) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Failed to recover account, code invalid` + }); + } + + const newUser = await prisma.user.update({ + where: { id: user.id }, + data: { + password: hashPassword(input.password), + code: null, + codeGeneratedAt: null, + confirmed: true + } + }) + + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + + return { status: true } + + }), + + register: publicProcedure.input( + z.object({ + username: z.string(), + email: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + + + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.email }, { username: input.username, }] + } + }); + + if (user) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `ACCOUNT_EXISTS`, + }); + } + + const code = generateOtp(); + const newUser = await prisma.user.create({ + select: { + ...defaultUserSelect, email: true + }, + data: { + username: input.username, + email: input.email, + password: hashPassword(input.password), + code: code, + codeGeneratedAt: new Date(), + confirmed: false, + role: "Teacher" + } + }) + + await sendConfirmationCode({ username: newUser.username, email: input.email, code: code }); + + return newUser + + }), + registerAsStudent: publicProcedure.input( + z.object({ + username: z.string(), + shareCode: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + + + const project = await prisma.project.findUnique({ + where: { shareCode: input.shareCode } + + }); + + if (!project) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `CODE_NOT_FOUND`, + }); + } + + + const user = await prisma.user.findFirst({ + select: defaultUserSelect, + where: { + username: input.username + } + }); + + if (user) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `ACCOUNT_EXISTS`, + }); + } + + + const newUser = await prisma.user.create({ + select: { + ...defaultUserSelect + }, + data: { + username: input.username, + password: hashPassword(input.password), + confirmed: true, + role: "Student" + } + }) + + await prisma.project.update({ + where: { id: project.id }, + data: { + members: { + create: [{ + userId: newUser.id, + }], + } + } + }) + + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + + return { projectId: project.id } + + }), + join: protectedProcedure.input( + z.object({ + shareCode: z.string(), + }), + ).mutation(async ({ ctx, input }) => { + + const project = await prisma.project.findUnique({ + where: { shareCode: input.shareCode } + + }); + + if (!project) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `CODE_NOT_FOUND`, + }); + } + + await prisma.project.update({ + where: { id: project.id }, + data: { + members: { + create: [{ + userId: ctx.user?.id + }], + } + } + }) + return { projectId: project.id } + }), + + askEmailConfirm: publicProcedure.input( + z.object({ + email: z.string() + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true }, + where: { + OR: [{ email: input.email }, { username: input.email, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + if (!user.email) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `User account doesn't have email address`, + }); + } + + const otp = generateOtp(); + await prisma.user.update({ + where: { id: user.id }, + data: { + code: otp, + codeGeneratedAt: new Date() + } + }) + await sendConfirmationCode({ email: user.email, username: user.username, code: otp }) + return { status: true } + }), + + confirm: publicProcedure.input( z.object({ username: z.string(), - password: z.string() + code: z.string(), }), - ).mutation(async ({ input }) => { - ; - throw new TRPCError({ - code: 'UNAUTHORIZED', + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + + if (!compareCodes(input.code, user.code || "")) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Failed to confirm account, code invalid` + }); + } + + const newUser = await prisma.user.update({ + where: { id: user.id }, + data: { + code: null, + codeGeneratedAt: null, + confirmed: true + } + }) + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + return { status: true } }), list: protectedProcedure.query(async () => { @@ -41,6 +392,7 @@ export const userRouter = router({ if (ctx.user) { // Retrieve the user with the given ID const user = await prisma.user.findUnique({ + select: { ...defaultUserSelect, email: true }, where: { id: ctx.user.id } }); return user; @@ -54,7 +406,7 @@ export const userRouter = router({ ).query(async (opts) => { const { input } = opts; // Retrieve the user with the given ID - const user = await prisma.user.findUnique({ where: { id: input.id } }); + const user = await prisma.user.findUnique({ select: defaultUserSelect, where: { id: input.id } }); return user; }), projects: protectedProcedure @@ -86,13 +438,7 @@ export const userRouter = router({ }, include: { user: { - select: { - id: true, - username: true, - role: true, - initial: true, - color: true - } + select: defaultUserSelect }, members: true, playlist: { diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index a631a35c..b15067a2 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -3,7 +3,7 @@ import "express-session" import { User, UserRole } from '@celluloid/prisma'; import { initTRPC, TRPCError } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; -import { Request } from "express"; +import { type Request, type Response } from 'express'; import { Session } from "express-session"; import { OpenApiMeta } from 'trpc-openapi'; import { v4 as uuid } from 'uuid'; @@ -13,6 +13,8 @@ export type Context = { requestId: string; requirePermissions: (roles: UserRole[]) => boolean; logout: () => Promise; + req: Request; + res: Response; }; export const createRPCContext = async ({ @@ -47,9 +49,13 @@ export const createRPCContext = async ({ }); }) } - return { user, requirePermissions, logout, requestId }; + return { + user, requirePermissions, logout, requestId, req, + res, + }; }; + const t = initTRPC.context().meta().create({ // transformer: SuperJSON }); diff --git a/packages/trpc/src/utils/forgot.ts b/packages/trpc/src/utils/forgot.ts new file mode 100644 index 00000000..a7d0c44f --- /dev/null +++ b/packages/trpc/src/utils/forgot.ts @@ -0,0 +1,19 @@ + +import bcrypt from 'bcryptjs'; + +export function hashPassword(password: string) { + const salt = bcrypt.genSaltSync(); + return bcrypt.hashSync(password, salt); +} + + +export function generateOtp() { + // Generate a 4-digit OTP + const otp: string = Math.floor(1000 + Math.random() * 9000).toString(); + return otp; +} + + +export function compareCodes(expected: string, actual: string) { + return expected.replace(/\s/g, "") === actual.replace(/\s/g, ""); +} diff --git a/packages/types/src/AnnotationTypes.ts b/packages/types/src/AnnotationTypes.ts deleted file mode 100644 index d61230fa..00000000 --- a/packages/types/src/AnnotationTypes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CommentRecord } from './CommentTypes'; -import { UserRecord } from './UserTypes'; - -export interface AnnotationData { - text: string; - startTime: number; - stopTime: number; - pause: boolean; -} - -export interface AnnotationRecord extends AnnotationData { - projectId: string; - userId: string; - id: string; - user: UserRecord; - comments: CommentRecord[]; -} \ No newline at end of file diff --git a/packages/types/src/CommentTypes.ts b/packages/types/src/CommentTypes.ts deleted file mode 100644 index c53bccb0..00000000 --- a/packages/types/src/CommentTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UserRecord } from './UserTypes'; - -export interface CommentRecord { - id: string; - annotationId: string; - userId: string; - text: string; - createdAt: Date; - user: UserRecord; -} \ No newline at end of file diff --git a/packages/types/src/ProjectTypes.ts b/packages/types/src/ProjectTypes.ts deleted file mode 100644 index e25b2c0a..00000000 --- a/packages/types/src/ProjectTypes.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { TagData } from './TagTypes'; -import { UserRecord } from './UserTypes'; - -export interface ProjectCreateData { - videoId: string; - title: string; - host: string; - description?: string; - objective: string; - assignments: Array; - tags: TagData[]; - levelStart: number; - levelEnd: number; - public: boolean; - collaborative: boolean; -} - -export interface ProjectUpdateData { - title: string; - description?: string; - objective: string; - assigments: Array; - tags: TagData[]; - levelStart: number; - leverEnd: number; - public: boolean; - collaborative: boolean; -} - -export interface ProjectRecord extends ProjectCreateData { - id: string; - userId: string; - publishedAt: Date; - shared: boolean; - shareName: string; - sharePassword: string; - shareExpiresAt: string; -} - -export interface ProjectGraphRecord extends ProjectRecord { - user: UserRecord; - members: UserRecord[]; -} - -export interface ProjectShareData { - sharePassword: string; - // shareExpiresAt: Date; - // shareMaxUsers: number; -} diff --git a/packages/types/src/TagTypes.ts b/packages/types/src/TagTypes.ts deleted file mode 100644 index ce2e89b5..00000000 --- a/packages/types/src/TagTypes.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface TagData { - id: string; - name: string; - featured: boolean; -} \ No newline at end of file diff --git a/packages/types/src/UnfurlTypes.ts b/packages/types/src/UnfurlTypes.ts deleted file mode 100644 index 977bf3a5..00000000 --- a/packages/types/src/UnfurlTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface UnfurlData { - website: string; - faviconUrl?: string; - title: string; - description?: string; - imageUrl?: string; -} \ No newline at end of file diff --git a/packages/types/src/UserTypes.ts b/packages/types/src/UserTypes.ts deleted file mode 100644 index 6bec594d..00000000 --- a/packages/types/src/UserTypes.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { TagData } from './TagTypes'; - -export interface SigninErrors { - login?: string; - password?: string; - email?: string; - username?: string; - confirmPassword?: string; - code?: string; - server?: string; - shareCode?: string; -} - -export interface SigninResult { - success: boolean; - errors: SigninErrors; -} - -export interface TeacherData { - email: string; - username: string; - subjects?: TagData[]; -} - -export interface TeacherRecord extends TeacherData { - id: string; -} - -export interface TeacherSignupData extends TeacherData { - password: string; -} - -export interface TeacherConfirmData { - login: string; - code: string; -} - -export interface Credentials { - login: string; - password: string; -} - -export interface StudentData { - username: string; -} - -export interface StudentRecord extends StudentData { - id: string; -} - -export interface StudentSignupData { - username: string; - password: string; - shareCode: string; -} - -export interface TeacherConfirmResetPasswordData extends TeacherConfirmData { - password: string; -} - -export interface ConfirmSignupErrors { - email: string; - code: string; -} - -export interface ConfirmSignupValidation { - success: boolean; - errors?: ConfirmSignupErrors; -} - -type UserRole - = 'Admin' - | 'Teacher' - | 'Student' - ; - -export interface UserRecord { - id: string; - username: string; - role: UserRole; - email?: string; -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fdf4de9b..bee8dd37 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,8 +1,2 @@ -export * from "./AnnotationTypes"; -export * from "./CommentTypes"; export * from "./PeerTubeVideo"; export * from "./PeerTubeVideoMetadata"; -export * from "./ProjectTypes"; -export * from "./TagTypes"; -export * from "./UnfurlTypes"; -export * from "./UserTypes"; diff --git a/packages/validators/package.json b/packages/validators/package.json deleted file mode 100644 index 313c15c3..00000000 --- a/packages/validators/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@celluloid/validators", - "main": "dist/index.js", - "version": "0.1.0", - "description": "Validation library for Celluloid client and server types", - "repository": "http://github.com/celluloid-edu/celluloid", - "author": "Younes Benaomar ", - "license": "MIT", - "scripts": { - "build": "tsup", - "dev": "tsup --watch --silent" - }, - "devDependencies": { - "@celluloid/types": "*", - "@types/validator": "13.7.10", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "validator": "13.7.0" - } -} diff --git a/packages/validators/src/UserValidator.ts b/packages/validators/src/UserValidator.ts deleted file mode 100644 index df4de555..00000000 --- a/packages/validators/src/UserValidator.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - Credentials, - SigninResult, - StudentSignupData, - TeacherConfirmData, - TeacherConfirmResetPasswordData, - TeacherSignupData, -} from '@celluloid/types'; -import validator from 'validator'; - -export function validateSignup(payload: TeacherSignupData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.username !== 'string' || - payload.username.trim().length === 0) { - result.success = false; - result.errors.username = `UsernameMissing`; - } - - if (!payload || typeof payload.email !== 'string' || - !validator.isEmail(payload.email)) { - result.success = false; - result.errors.email = 'InvalidEmailFormat'; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length < 8) { - result.success = false; - result.errors.password = 'InvalidPasswordFormat'; - } - return result; -} - -export function validateConfirmationCode(code: string): boolean { - const codeRegExp = /^[0-9]{6}$/; - const trimmedCode = code.replace(/\s/g, ''); - return codeRegExp.test(trimmedCode); -} - -export function validateConfirmResetPassword( - payload: TeacherConfirmResetPasswordData -) { - const result = { - success: true, - errors: {} - } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.login = 'MissingLogin'; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length < 8) { - result.success = false; - result.errors.password = 'InvalidPasswordFormat'; - } - - if (!payload || typeof payload.code !== 'string' || - !validateConfirmationCode(payload.code)) { - result.success = false; - result.errors.code = `InvalidCodeFormat`; - } - return result; -} - -export function validateConfirmSignup(payload: TeacherConfirmData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.email = 'MissingLogin'; - } - - if (!payload || typeof payload.code !== 'string' || - !validateConfirmationCode(payload.code)) { - result.success = false; - result.errors.code = `InvalidCodeFormat`; - } - - return result; -} - -export function validateLogin(payload: Credentials) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.login = `MissingLogin`; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length === 0) { - result.success = false; - result.errors.password = 'MissingPassword'; - } - - return result; -} - -export function validateStudentSignup(payload: StudentSignupData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.shareCode !== 'string' || - payload.shareCode.trim().length === 0) { - result.success = false; - result.errors.shareCode = `MissingShareCode`; - } - - if (!payload || typeof payload.username !== 'string' || - payload.username.trim().length === 0) { - result.success = false; - result.errors.username = `MissingUsername`; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length === 0) { - result.success = false; - result.errors.password = 'MissingPassword'; - } - - return result; -} diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts deleted file mode 100644 index 80e5c679..00000000 --- a/packages/validators/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './UserValidator'; \ No newline at end of file diff --git a/packages/validators/tsconfig.json b/packages/validators/tsconfig.json deleted file mode 100644 index 847685fc..00000000 --- a/packages/validators/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@celluloid/config/tsconfig/node16.json", - "include": [ - "**/*.ts", - "**/*.tsx", - "tsup.config.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/packages/validators/tsup.config.ts b/packages/validators/tsup.config.ts deleted file mode 100644 index db41265e..00000000 --- a/packages/validators/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig({ - clean: true, - dts: true, - entry: ["src/index.ts"], - format: ["esm", "cjs"], - minify: isProduction, - sourcemap: true, -}); diff --git a/yarn.lock b/yarn.lock index af7b4b9f..b9b80b6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1770,7 +1770,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.23.1": +"@babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.23.1": version: 7.23.2 resolution: "@babel/runtime@npm:7.23.2" dependencies: @@ -1918,9 +1918,11 @@ __metadata: resolution: "@celluloid/trpc@workspace:packages/trpc" dependencies: "@celluloid/config": "*" + "@celluloid/passport": "*" "@celluloid/prisma": "*" "@trpc/server": ^10.40.0 "@types/express-session": ^1.17.8 + "@types/mjml": ^4.7.3 "@types/uuid": ^9.0.4 bcryptjs: ^2.4.3 change-case: ^4.1.2 @@ -1929,6 +1931,9 @@ __metadata: express-session: ^1.17.3 js2xmlparser: ^5.0.0 lodash: ^4.17.21 + mjml: ^4.14.1 + nodemailer: ^6.9.7 + nodemailer-smtp-transport: ^2.7.4 papaparse: ^5.4.1 trpc-openapi: ^1.2.0 tsup: ^7.2.0 @@ -3560,6 +3565,13 @@ __metadata: languageName: node linkType: hard +"@one-ini/wasm@npm:0.1.1": + version: 0.1.1 + resolution: "@one-ini/wasm@npm:0.1.1" + checksum: 11de17108eae57c797e552e36b259398aede999b4a689d78be6459652edc37f3428472410590a9d328011a8751b771063a5648dd5c4205631c55d1d58e313156 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3709,26 +3721,6 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:^1.8.6": - version: 1.9.6 - resolution: "@reduxjs/toolkit@npm:1.9.6" - dependencies: - immer: ^9.0.21 - redux: ^4.2.1 - redux-thunk: ^2.4.2 - reselect: ^4.1.8 - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - checksum: 61d445f7e084c79f9601f61fcfc4eb65152b850b2a4330239d982297605bd870e63dc1e0211deb3822392cd3bc0c88ca0cdb236a9711a4311dfb199c607b6ac5 - languageName: node - linkType: hard - "@remirror/core-constants@npm:^2.0.2": version: 2.0.2 resolution: "@remirror/core-constants@npm:2.0.2" @@ -5507,6 +5499,22 @@ __metadata: languageName: node linkType: hard +"@types/mjml-core@npm:*": + version: 4.7.3 + resolution: "@types/mjml-core@npm:4.7.3" + checksum: c6d002cc599806a9603e48f3b848a48c54a01a9b8cbc147b1cf01355e80a0ed1be720928e9e290c0dc876ba826fb1f84e7620d8e6edf53b48ce27fa06a73e6e2 + languageName: node + linkType: hard + +"@types/mjml@npm:^4.7.3": + version: 4.7.3 + resolution: "@types/mjml@npm:4.7.3" + dependencies: + "@types/mjml-core": "*" + checksum: c7d31acaea2495cd58bb3ce89f0cb4e52e938be6cfb4262f5be4a3887f24eed50674324d5a25a1797e48c1c7ee295dc2e21c73d45f47b02f1d0a6bcfc14009e4 + languageName: node + linkType: hard + "@types/moment-duration-format@npm:^2.2.0": version: 2.2.4 resolution: "@types/moment-duration-format@npm:2.2.4" @@ -6789,6 +6797,13 @@ __metadata: languageName: node linkType: hard +"ansi-colors@npm:^4.1.1": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: a9c2ec842038a1fabc7db9ece7d3177e2fe1c5dc6f0c51ecfbf5f39911427b89c00b5dc6b8bd95f82a26e9b16aaae2e83d45f060e98070ce4d1333038edceb0e + languageName: node + linkType: hard + "ansi-escapes@npm:^3.0.0, ansi-escapes@npm:^3.2.0": version: 3.2.0 resolution: "ansi-escapes@npm:3.2.0" @@ -8020,6 +8035,16 @@ __metadata: languageName: node linkType: hard +"camel-case@npm:^3.0.0": + version: 3.0.0 + resolution: "camel-case@npm:3.0.0" + dependencies: + no-case: ^2.2.0 + upper-case: ^1.1.1 + checksum: 4190ed6ab8acf4f3f6e1a78ad4d0f3f15ce717b6bfa1b5686d58e4bcd29960f6e312dd746b5fa259c6d452f1413caef25aee2e10c9b9a580ac83e516533a961a + languageName: node + linkType: hard + "camel-case@npm:^4.1.2": version: 4.1.2 resolution: "camel-case@npm:4.1.2" @@ -8313,7 +8338,7 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:^1.0.0-rc.2, cheerio@npm:^1.0.0-rc.3": +"cheerio@npm:1.0.0-rc.12, cheerio@npm:^1.0.0-rc.12, cheerio@npm:^1.0.0-rc.2, cheerio@npm:^1.0.0-rc.3": version: 1.0.0-rc.12 resolution: "cheerio@npm:1.0.0-rc.12" dependencies: @@ -8328,7 +8353,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:^3.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -8389,6 +8414,15 @@ __metadata: languageName: node linkType: hard +"clean-css@npm:^4.2.1": + version: 4.2.4 + resolution: "clean-css@npm:4.2.4" + dependencies: + source-map: ~0.6.0 + checksum: 045ff6fcf4b5c76a084b24e1633e0c78a13b24080338fc8544565a9751559aa32ff4ee5886d9e52c18a644a6ff119bd8e37bc58e574377c05382a1fb7dbe39f8 + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.2 resolution: "clean-css@npm:5.3.2" @@ -8768,7 +8802,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.0": +"commander@npm:^6.1.0, commander@npm:^6.2.0": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 @@ -8899,6 +8933,16 @@ __metadata: languageName: node linkType: hard +"config-chain@npm:^1.1.13": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: ^1.3.4 + proto-list: ~1.2.1 + checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab + languageName: node + linkType: hard + "configstore@npm:^5.0.1": version: 5.0.1 resolution: "configstore@npm:5.0.1" @@ -8936,29 +8980,6 @@ __metadata: languageName: node linkType: hard -"connected-react-router@npm:6.9.3": - version: 6.9.3 - resolution: "connected-react-router@npm:6.9.3" - dependencies: - immutable: ^3.8.1 || ^4.0.0 - lodash.isequalwith: ^4.4.0 - prop-types: ^15.7.2 - seamless-immutable: ^7.1.3 - peerDependencies: - history: ^4.7.2 - react: ^16.4.0 || ^17.0.0 - react-redux: ^6.0.0 || ^7.1.0 - react-router: ^4.3.1 || ^5.0.0 - redux: ^3.6.0 || ^4.0.0 - dependenciesMeta: - immutable: - optional: true - seamless-immutable: - optional: true - checksum: 047a11c2f3c9993087f3cd467789445781320c61f3184e1016e7b05862a275006004867231cc7396c7f213afd2fbce7e8ac0df39ba2cda7502d72a140657f9e7 - languageName: node - linkType: hard - "consola@npm:^3.2.3": version: 3.2.3 resolution: "consola@npm:3.2.3" @@ -10046,6 +10067,13 @@ __metadata: languageName: node linkType: hard +"detect-node@npm:2.0.4": + version: 2.0.4 + resolution: "detect-node@npm:2.0.4" + checksum: c06ae40fefbad8cb8cbb6ca819c93568b2a809e747bfc9c71f3524b027f5e988163b0ac0517fd65288b375360b30bc4822172eb05d211f99003d73cf8ec22911 + languageName: node + linkType: hard + "detect-node@npm:^2.0.4": version: 2.1.0 resolution: "detect-node@npm:2.1.0" @@ -10255,6 +10283,15 @@ __metadata: languageName: node linkType: hard +"domhandler@npm:^3.3.0": + version: 3.3.0 + resolution: "domhandler@npm:3.3.0" + dependencies: + domelementtype: ^2.0.1 + checksum: 850e5e9fee7834ab4314811e18bc1f4294d7eafbf6a79ad03cbe50cf964108935c97257ac248944d72a9312b4a18dfa8323e857d23278964dc83b1f124467fa3 + languageName: node + linkType: hard + "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": version: 4.3.1 resolution: "domhandler@npm:4.3.1" @@ -10283,7 +10320,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^2.5.2, domutils@npm:^2.8.0": +"domutils@npm:^2.4.2, domutils@npm:^2.5.2, domutils@npm:^2.8.0": version: 2.8.0 resolution: "domutils@npm:2.8.0" dependencies: @@ -10436,6 +10473,20 @@ __metadata: languageName: node linkType: hard +"editorconfig@npm:^1.0.3": + version: 1.0.4 + resolution: "editorconfig@npm:1.0.4" + dependencies: + "@one-ini/wasm": 0.1.1 + commander: ^10.0.0 + minimatch: 9.0.1 + semver: ^7.5.3 + bin: + editorconfig: bin/editorconfig + checksum: 09904f19381b3ddf132cea0762971aba887236f387be3540909e96b8eb9337e1793834e10f06890cd8e8e7bb1ba80cb13e7d50a863f227806c9ca74def4165fb + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -12305,7 +12356,6 @@ __metadata: "@mui/lab": ^5.0.0-alpha.148 "@mui/material": ^5.14.13 "@mui/styles": ^5.14.13 - "@reduxjs/toolkit": ^1.8.6 "@tanstack/react-query": ^4.36.1 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^13.4.0 @@ -12343,7 +12393,6 @@ __metadata: autosuggest-highlight: ^3.3.4 axios: ^1.3.4 change-case: ^4.1.2 - connected-react-router: 6.9.3 copy-to-clipboard: ^3.3.3 dayjs: ^1.11.10 enzyme: ^3.3.0 @@ -12365,7 +12414,6 @@ __metadata: notistack: ^3.0.1 passport: ^0.6.0 passport-local: ^1.0.0 - prop-types: ^15.6.2 query-string: ^6.1.0 ramda: ^0.28.0 randomcolor: ^0.5.3 @@ -12377,16 +12425,12 @@ __metadata: react-error-boundary: ^4.0.11 react-full-screen: ^0.2.2 react-i18next: ^13.2.2 - react-redux: ^8.0.4 react-router: ^6.17.0 react-router-dom: ^6.17.0 react-scripts: 5.0.1 react-transition-group: ^2.3.1 react-use-event: ^1.1.1 recoil: ^0.7.7 - redux: ^4.0.0 - redux-devtools-extension: ^2.13.5 - redux-thunk: ^2.3.0 rooks: ^7.4.1 serve: ^14.2.1 shiitake: ^3.0.2 @@ -12394,6 +12438,7 @@ __metadata: vite: ^4.4.11 vite-aliases: ^0.11.3 yup: ^1.3.2 + yup-locales: ^1.2.18 languageName: unknown linkType: soft @@ -12852,7 +12897,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0, glob@npm:^8.0.3": +"glob@npm:^8.0.0, glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -13416,6 +13461,23 @@ __metadata: languageName: node linkType: hard +"html-minifier@npm:^4.0.0": + version: 4.0.0 + resolution: "html-minifier@npm:4.0.0" + dependencies: + camel-case: ^3.0.0 + clean-css: ^4.2.1 + commander: ^2.19.0 + he: ^1.2.0 + param-case: ^2.1.1 + relateurl: ^0.2.7 + uglify-js: ^3.5.1 + bin: + html-minifier: ./cli.js + checksum: b426aee771d9da104c1c9554e3ebd3a4f483d2ce01f4dcc4156ba33a5959044acf6bea192d5ae63b290cdb92c30a9d07fd6924c65609aa82382ce411328f94ca + languageName: node + linkType: hard + "html-parse-stringify@npm:^3.0.1": version: 3.0.1 resolution: "html-parse-stringify@npm:3.0.1" @@ -13447,6 +13509,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^5.0.0": + version: 5.0.1 + resolution: "htmlparser2@npm:5.0.1" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^3.3.0 + domutils: ^2.4.2 + entities: ^2.0.0 + checksum: b67ac02e44629ec76b712fc06702451bea64e522cfcd7cc22fa85023b81b44cde5060662faa81d34f18c0fe5a43ced1cac73528d30a6df5ac5825a4d479c7ea5 + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -13790,14 +13864,14 @@ __metadata: languageName: node linkType: hard -"immer@npm:^9.0.21, immer@npm:^9.0.7": +"immer@npm:^9.0.7": version: 9.0.21 resolution: "immer@npm:9.0.21" checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 languageName: node linkType: hard -"immutable@npm:^3.8.1 || ^4.0.0, immutable@npm:^4.2.2": +"immutable@npm:^4.2.2": version: 4.3.4 resolution: "immutable@npm:4.3.4" checksum: de3edd964c394bab83432429d3fb0b4816b42f56050f2ca913ba520bd3068ec3e504230d0800332d3abc478616e8f55d3787424a90d0952e6aba864524f1afc3 @@ -15535,6 +15609,22 @@ __metadata: languageName: node linkType: hard +"js-beautify@npm:^1.6.14": + version: 1.14.9 + resolution: "js-beautify@npm:1.14.9" + dependencies: + config-chain: ^1.1.13 + editorconfig: ^1.0.3 + glob: ^8.1.0 + nopt: ^6.0.0 + bin: + css-beautify: js/bin/css-beautify.js + html-beautify: js/bin/html-beautify.js + js-beautify: js/bin/js-beautify.js + checksum: aea5af03d0e8d5bcdfc9f98d6c6ebdc17076c762123ae79557d271a921438e2c0c422bc56a955119d770bb0f01cb411003534d8ae8dc138eb7af4821f21f8352 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -15871,6 +15961,21 @@ __metadata: languageName: node linkType: hard +"juice@npm:^9.0.0": + version: 9.1.0 + resolution: "juice@npm:9.1.0" + dependencies: + cheerio: ^1.0.0-rc.12 + commander: ^6.1.0 + mensch: ^0.3.4 + slick: ^1.12.2 + web-resource-inliner: ^6.0.1 + bin: + juice: bin/juice + checksum: 95f20fa183baa17360d7f03f2699f7cbc3476fb2e3a2d1d81d28f2ce1e5cd61a634a05cad26cfe83174c730ecbde18d8db9bc244b915741833fa6ce1c61c6864 + languageName: node + linkType: hard + "jw-paginate@npm:^1.0.4": version: 1.0.4 resolution: "jw-paginate@npm:1.0.4" @@ -16350,13 +16455,6 @@ __metadata: languageName: node linkType: hard -"lodash.isequalwith@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.isequalwith@npm:4.4.0" - checksum: 428ba7a57c47ec05e2dd18c03a4b4c45dac524a46af7ce3f412594bfc7be6a5acaa51acf9ea113d0002598e9aafc6e19ee8d20bc28363145fcb4d21808c9039f - languageName: node - linkType: hard - "lodash.isfunction@npm:^3.0.9": version: 3.0.9 resolution: "lodash.isfunction@npm:3.0.9" @@ -16526,6 +16624,13 @@ __metadata: languageName: node linkType: hard +"lower-case@npm:^1.1.1": + version: 1.1.4 + resolution: "lower-case@npm:1.1.4" + checksum: 1ca9393b5eaef94a64e3f89e38b63d15bc7182a91171e6ad1550f51d710ec941540a065b274188f2e6b4576110cc2d11b50bc4bb7c603a040ddeb1db4ca95197 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -16829,6 +16934,13 @@ __metadata: languageName: node linkType: hard +"mensch@npm:^0.3.4": + version: 0.3.4 + resolution: "mensch@npm:0.3.4" + checksum: eabb25d595b9bb7c067b932ea9c96f0c8154a4bb6c454a4edecef9f5c87652e345a40128741ed95905699c5a16ad1f6c7efd5f6dfc06e18128d74b569a4fb893 + languageName: node + linkType: hard + "meow@npm:^10.1.3": version: 10.1.5 resolution: "meow@npm:10.1.5" @@ -16947,6 +17059,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^2.4.6": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 1497ba7b9f6960694268a557eae24b743fd2923da46ec392b042469f4b901721ba0adcf8b0d3c2677839d0e243b209d76e5edcbd09cfdeffa2dfb6bb4df4b862 + languageName: node + linkType: hard + "mime@npm:^3.0.0": version: 3.0.0 resolution: "mime@npm:3.0.0" @@ -17032,6 +17153,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -17168,6 +17298,408 @@ __metadata: languageName: node linkType: hard +"mjml-accordion@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-accordion@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 66212dcf89531da230115c786dec24194d8ec9a4c93bcc1cfdbac332be07678eee3b8479d46f155cb60bf13358edd5cd7e4d6538ad5f9a910cbee5bb6b450855 + languageName: node + linkType: hard + +"mjml-body@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-body@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 27388e15681bb25412a7123ae82559e6cb5586293aef3aa2cf57138bee401c1b53e84d8efacef2c9db4cb7bf8dc8cac741b7907ec11036f2b804178db511301c + languageName: node + linkType: hard + +"mjml-button@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-button@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 55fa3228476fbb17c51d63fbc9a18ce280c3246a69164bbd6d93f4670b3a9f93e985cece5958cc94ff0b60fbc199bd1382fd85d27aac0677517926ec8dd0ad6f + languageName: node + linkType: hard + +"mjml-carousel@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-carousel@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: db6d7847722ef1d4fd2b74aba04853156c729ba1a99e5565fbe5c32ed96733de1846fc41995505ec950de4953fa415586251c1e65f731725edd9d4b08b259e87 + languageName: node + linkType: hard + +"mjml-cli@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-cli@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + chokidar: ^3.0.0 + glob: ^7.1.1 + html-minifier: ^4.0.0 + js-beautify: ^1.6.14 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + yargs: ^16.1.0 + bin: + mjml-cli: bin/mjml + checksum: ed3a08c68b6c5261e173674d1f1276b2cd636f2edc8713234a071befe919f9f9aa22e254480516d4b8d49eef22989017ce4327418c1c03fe08b004b6d1f8d136 + languageName: node + linkType: hard + +"mjml-column@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-column@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: a8eb4f321b9015ba8be96d08019502ada557fe3ba55413abf71b39a9ce209d0a3550ba03714a91b03b065af8b64582f6b3703b249f77c12bc1b54a499ac10ee2 + languageName: node + linkType: hard + +"mjml-core@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-core@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + cheerio: 1.0.0-rc.12 + detect-node: ^2.0.4 + html-minifier: ^4.0.0 + js-beautify: ^1.6.14 + juice: ^9.0.0 + lodash: ^4.17.21 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + checksum: fe46769b1746b1da90ddd39c584a6c8f7db80e125e079ce83cfd8ab4888e5abfff2933f573993926b36721de194b261c28f078b9316c395b185fd4098298c025 + languageName: node + linkType: hard + +"mjml-divider@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-divider@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b44fab9de9751caf626ca6206b58c2a9ac7788c54c56d91cc892f77ed164a0fd2021422ef1019adb147a145873db499bb89f1518aa4326face1135acd8f61294 + languageName: node + linkType: hard + +"mjml-group@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-group@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 17cec7be9544ae32121cac12cf6dffd0a234bb2c14e2e72df230c52f1834f34fbd2df6ed15661491d0eee2c95dd7ad77e6048ca0ca012c9970d96bcfe8a4e77e + languageName: node + linkType: hard + +"mjml-head-attributes@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-attributes@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 93783e5ce4df95c745fee65cf2a4787eadbd548bb2d35f4c408d50cd4f81652061da4fcf54b4861db40bef115b60bb29f36faf6478033ad32e5e467415ec394a + languageName: node + linkType: hard + +"mjml-head-breakpoint@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-breakpoint@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b52ea526f9291e0919ec82a7cc89e1e4d5a22c78280bc039b97648a3b938778d3bc7ff77b658a8a5d247c80327d2677f1591a5638039edc0d7c6f86670a1aeec + languageName: node + linkType: hard + +"mjml-head-font@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-font@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 3787977d042634ed338eb5d1be8612494a55419f568187c40517d3c53d57a93d3efd13c82c89d4a5b5c6456082bee12b6f682ededbc24a071600c9986a88ee94 + languageName: node + linkType: hard + +"mjml-head-html-attributes@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-html-attributes@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: a076f05954e09d7d8d721dc6931a1ecfcfa59126d4c7859c6278404d8e036b83f8eb72fd4285f367324d170bde7df64385ddf093b9f47cf5115fffd85756a510 + languageName: node + linkType: hard + +"mjml-head-preview@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-preview@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 9d8301458a93794695a1c50a16dbdd7914c008f0a89ee87be9d83f494966fb0aa51434549a6f183a014e34bfdc23795607bc33a33a1a4225882c8d0208fa3898 + languageName: node + linkType: hard + +"mjml-head-style@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-style@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 2e96180bd72656c70507f21a37f8cf3c0dc41709052af42e1161d77551df762f62d863635c18dff6d092bab9bd8c8c631c0a09b3c6dc25575f0693ee6627b7ba + languageName: node + linkType: hard + +"mjml-head-title@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-title@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 65dfe9cb5115a9cfe76851b9e5aabaaa30131e55a4346e9ec04bde3234897ffe1ab3e7bb37a695af44deffe4a869dee34668a3d87396ed50b923310fb9baebcd + languageName: node + linkType: hard + +"mjml-head@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: c83c930badb7ad0ee5771b13928d2c371aa9b70777393e32361fa356b534d1b282f5698e41dee8f947c687d28580e80b74bac2d3308970884e58152edc86bafd + languageName: node + linkType: hard + +"mjml-hero@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-hero@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 6c6ec8e5168709f09175d030c2a6fc7326f7a2e076cf09c0676e78bd941521e2c4295335bfdce8b5c31ea946a1925edcb780aced73b0dbfda40c07a463526c93 + languageName: node + linkType: hard + +"mjml-image@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-image@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 1ad0910300b115fcc42de6d642ce35b2a3593ac1a431498a2a2f3210733ff7c2e4bc33334abbd20f9854c77aa0f7c859928941fa6cb0bce190453f857e7c7f90 + languageName: node + linkType: hard + +"mjml-migrate@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-migrate@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + js-beautify: ^1.6.14 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-parser-xml: 4.14.1 + yargs: ^16.1.0 + bin: + migrate: lib/cli.js + checksum: 6710d100d79fd0b066cfd2fd0a5f7e6d7ccbf309a31039f162a22ff7b69c0540e550325560737270b205a3a3cd4562603e6bc4a44424ca973c44168741c3f388 + languageName: node + linkType: hard + +"mjml-navbar@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-navbar@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b85ccb20a95575387b5ce65e317f9a25fe46c1d77bab506274d630950da6bcbec1034cf351887eb1aec10e6c0b8b926804fc20cbed99209de45d49ada736f969 + languageName: node + linkType: hard + +"mjml-parser-xml@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-parser-xml@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + detect-node: 2.0.4 + htmlparser2: ^8.0.1 + lodash: ^4.17.15 + checksum: 839225d2d8c5b7c8a948ebe2a49afa8aa8f4e3651810b40df95d6f39da56ea6d62e2c4e5c55f96eb60d191233c0d2c77be0ee9cc861ffa5c3da032be56e0d96c + languageName: node + linkType: hard + +"mjml-preset-core@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-preset-core@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + mjml-accordion: 4.14.1 + mjml-body: 4.14.1 + mjml-button: 4.14.1 + mjml-carousel: 4.14.1 + mjml-column: 4.14.1 + mjml-divider: 4.14.1 + mjml-group: 4.14.1 + mjml-head: 4.14.1 + mjml-head-attributes: 4.14.1 + mjml-head-breakpoint: 4.14.1 + mjml-head-font: 4.14.1 + mjml-head-html-attributes: 4.14.1 + mjml-head-preview: 4.14.1 + mjml-head-style: 4.14.1 + mjml-head-title: 4.14.1 + mjml-hero: 4.14.1 + mjml-image: 4.14.1 + mjml-navbar: 4.14.1 + mjml-raw: 4.14.1 + mjml-section: 4.14.1 + mjml-social: 4.14.1 + mjml-spacer: 4.14.1 + mjml-table: 4.14.1 + mjml-text: 4.14.1 + mjml-wrapper: 4.14.1 + checksum: 86852c543c138fcafecd461ccecd03c36b0ac573a644fe47a164b8f94465c33eee25c815e6cb17a85bd947bccd21ffb700023a22d1f39e5540ba9b663c96e7ce + languageName: node + linkType: hard + +"mjml-raw@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-raw@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 65721432a89653644ae7e451e5a09d5168e6a69900f73823b74803ac4f4ff148ee4654db916e770e9e7a4e51cb83222c95b15b0220f21d96eeb9eb1a8571be7d + languageName: node + linkType: hard + +"mjml-section@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-section@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: f4b2ba3fa916193635b273d482a23e6f2f2969d01b5517e62d505ef5b6260e404bd2df3252ebd5926c1d5dc79f33cac8ceab19c161cf8435c3a23148c0296a15 + languageName: node + linkType: hard + +"mjml-social@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-social@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 4d493dcb133beb6361cc5b6ff5799ef8456e39fd89f60d1c8ecc8767eb2fcfedf5f0a253dfafa543c6c3a32a798cb3e009b59e6588fdce5726b057435cf5d3a6 + languageName: node + linkType: hard + +"mjml-spacer@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-spacer@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 93bf08f18da4a6593ded0675d32d0b2599d8fa9b00a3f3c0d90803106611f09a48efff803f82e740e27c8e5e56a36a40c66c87045ca7090ca5685762f0fe9382 + languageName: node + linkType: hard + +"mjml-table@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-table@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: f7fc1f648a112b8bab5209e9a3200926b1c10b39acc90f691e6b2e6d75a642ebf2f8f603b72676bd3490c3afaad97d06f1a64503dd971695f431760436317b26 + languageName: node + linkType: hard + +"mjml-text@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-text@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 16133c363813a4ec5bef06fbd34789a59d06206f78c43e43f1979bb326169b9f0809c4ddf651a05ae8ea4b295dcce6ad80d6c696b628832a5357d3bb532a2d5d + languageName: node + linkType: hard + +"mjml-validator@npm:4.13.0": + version: 4.13.0 + resolution: "mjml-validator@npm:4.13.0" + dependencies: + "@babel/runtime": ^7.14.6 + checksum: 40397cc664ee0e1ad884ddef30e2ab1cb3b14bb3fb1730e9ba8d7a786c25a260726b4bb70bae7094aa4177a369fd46bd2bf7f8e744f9cdecd0c3ceb8881b075e + languageName: node + linkType: hard + +"mjml-wrapper@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-wrapper@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-section: 4.14.1 + checksum: c3421fe6d783b4dfe617b37eae21aa3ff6e345ad06e18e8aeddd91e70bea75d277004feaf39d9af298e6e3ee550553df5110121d4486e1610ad51ae61a5ddf07 + languageName: node + linkType: hard + +"mjml@npm:^4.14.1": + version: 4.14.1 + resolution: "mjml@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + mjml-cli: 4.14.1 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-preset-core: 4.14.1 + mjml-validator: 4.13.0 + bin: + mjml: bin/mjml + checksum: 48906b077ea7283f77cec0baec422ebee133a5a2ea2c727c31e35f4b4e56894ef3134fb317704c12e4bc40632321779df19b950555bc49d188675e84dca7a826 + languageName: node + linkType: hard + "mkdirp@npm:1.0.4, mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -17380,6 +17912,15 @@ __metadata: languageName: node linkType: hard +"no-case@npm:^2.2.0": + version: 2.3.2 + resolution: "no-case@npm:2.3.2" + dependencies: + lower-case: ^1.1.1 + checksum: 856487731936fef44377ca74fdc5076464aba2e0734b56a4aa2b2a23d5b154806b591b9b2465faa59bb982e2b5c9391e3685400957fb4eeb38f480525adcf3dd + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -17397,7 +17938,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": +"node-fetch@npm:2, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -17512,6 +18053,13 @@ __metadata: languageName: node linkType: hard +"nodemailer@npm:^6.9.7": + version: 6.9.7 + resolution: "nodemailer@npm:6.9.7" + checksum: 0cf66d27aed3bd2cbdff9939402cec3d2119c31b2b9ff4af3bcd59f48287ea75b90c0ce2cd9eb0df838164972cd25581b4b723c91fd673e2608bcb28445ccb1b + languageName: node + linkType: hard + "noms@npm:0.0.0": version: 0.0.0 resolution: "noms@npm:0.0.0" @@ -18301,6 +18849,15 @@ __metadata: languageName: node linkType: hard +"param-case@npm:^2.1.1": + version: 2.1.1 + resolution: "param-case@npm:2.1.1" + dependencies: + no-case: ^2.2.0 + checksum: 3a63dcb8d8dc7995a612de061afdc7bb6fe7bd0e6db994db8d4cae999ed879859fd24389090e1a0d93f4c9207ebf8c048c870f468a3f4767161753e03cb9ab58 + languageName: node + linkType: hard + "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -20119,6 +20676,13 @@ __metadata: languageName: node linkType: hard +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: 4d4826e1713cbfa0f15124ab0ae494c91b597a3c458670c9714c36e8baddf5a6aad22842776f2f5b137f259c8533e741771445eb8df82e861eea37a6eaba03f7 + languageName: node + linkType: hard + "proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -20824,7 +21388,7 @@ __metadata: languageName: node linkType: hard -"react-redux@npm:^8.0.4, react-redux@npm:^8.0.5, react-redux@npm:^8.1.1": +"react-redux@npm:^8.0.5, react-redux@npm:^8.1.1": version: 8.1.2 resolution: "react-redux@npm:8.1.2" dependencies: @@ -21270,24 +21834,6 @@ __metadata: languageName: node linkType: hard -"redux-devtools-extension@npm:^2.13.5": - version: 2.13.9 - resolution: "redux-devtools-extension@npm:2.13.9" - peerDependencies: - redux: ^3.1.0 || ^4.0.0 - checksum: 603d48fd6acf3922ef373b251ab3fdbb990035e90284191047b29d25b06ea18122bc4ef01e0704ccae495acb27ab5e47b560937e98213605dd88299470025db9 - languageName: node - linkType: hard - -"redux-thunk@npm:^2.3.0, redux-thunk@npm:^2.4.2": - version: 2.4.2 - resolution: "redux-thunk@npm:2.4.2" - peerDependencies: - redux: ^4 - checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c - languageName: node - linkType: hard - "redux@npm:^4.0.0, redux@npm:^4.2.1": version: 4.2.1 resolution: "redux@npm:4.2.1" @@ -21527,13 +22073,6 @@ __metadata: languageName: node linkType: hard -"reselect@npm:^4.1.8": - version: 4.1.8 - resolution: "reselect@npm:4.1.8" - checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e - languageName: node - linkType: hard - "resize-observer-polyfill@npm:^1.5.1": version: 1.5.1 resolution: "resize-observer-polyfill@npm:1.5.1" @@ -22124,13 +22663,6 @@ __metadata: languageName: node linkType: hard -"seamless-immutable@npm:^7.1.3": - version: 7.1.4 - resolution: "seamless-immutable@npm:7.1.4" - checksum: f65c1dc12e460265ccc4b164085b807570f9fb8a619cd3c216fc7ed933fb09c57a24a7df1b638dc9bd6367d8d69c2f00b5370b0c0996b4046242539096d2d0c6 - languageName: node - linkType: hard - "section-iterator@npm:^2.0.0": version: 2.0.0 resolution: "section-iterator@npm:2.0.0" @@ -22543,6 +23075,13 @@ __metadata: languageName: node linkType: hard +"slick@npm:^1.12.2": + version: 1.12.2 + resolution: "slick@npm:1.12.2" + checksum: 02b586dac1ce12db4e6d3b89e61962e6e07966875b8099ba1d6fac2faa8c88f37450293c27706f296556e4698b9e139bfee4055b845a7c266eca4650609d7603 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -24466,6 +25005,15 @@ __metadata: languageName: node linkType: hard +"uglify-js@npm:^3.5.1": + version: 3.17.4 + resolution: "uglify-js@npm:3.17.4" + bin: + uglifyjs: bin/uglifyjs + checksum: 7b3897df38b6fc7d7d9f4dcd658599d81aa2b1fb0d074829dd4e5290f7318dbca1f4af2f45acb833b95b1fe0ed4698662ab61b87e94328eb4c0a0d3435baf924 + languageName: node + linkType: hard + "uid-safe@npm:~2.1.5": version: 2.1.5 resolution: "uid-safe@npm:2.1.5" @@ -24723,6 +25271,13 @@ __metadata: languageName: node linkType: hard +"upper-case@npm:^1.1.1": + version: 1.1.3 + resolution: "upper-case@npm:1.1.3" + checksum: 991c845de75fa56e5ad983f15e58494dd77b77cadd79d273cc11e8da400067e9881ae1a52b312aed79b3d754496e2e0712e08d22eae799e35c7f9ba6f3d8a85d + languageName: node + linkType: hard + "upper-case@npm:^2.0.2": version: 2.0.2 resolution: "upper-case@npm:2.0.2" @@ -24937,6 +25492,13 @@ __metadata: languageName: node linkType: hard +"valid-data-url@npm:^3.0.0": + version: 3.0.1 + resolution: "valid-data-url@npm:3.0.1" + checksum: 06584294fb4c9550f0aaa56470f8d748f4ebfc3ed230707db5559754719a66fc37f299b5a79b914375b8198d90f8a51e0401375391938caf8dc8e442308aab9e + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -25268,6 +25830,20 @@ __metadata: languageName: node linkType: hard +"web-resource-inliner@npm:^6.0.1": + version: 6.0.1 + resolution: "web-resource-inliner@npm:6.0.1" + dependencies: + ansi-colors: ^4.1.1 + escape-goat: ^3.0.0 + htmlparser2: ^5.0.0 + mime: ^2.4.6 + node-fetch: ^2.6.0 + valid-data-url: ^3.0.0 + checksum: 17d9e53a6e5f07361abc584b6bb2bb8470978be580f8b5cdcab5998507ffccf5fb645616d3fe1550965d2db497f4a5cdc1ea1460c9cf464de315751962708ecc + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -26088,6 +26664,13 @@ __metadata: languageName: node linkType: hard +"yup-locales@npm:^1.2.18": + version: 1.2.18 + resolution: "yup-locales@npm:1.2.18" + checksum: e929555df880532a31973f693a0ae600522748ec08b6d0d4eff4a0d1f4af9f1d368712d74090a4b0025125e68cf95ab5d0abb9a55619f589c611181e2768dae0 + languageName: node + linkType: hard + "yup@npm:^1.3.2": version: 1.3.2 resolution: "yup@npm:1.3.2"
(type: ActionType, payload: P): - Required> { - return { type, payload, error: false }; -} - -export function createOptionalAction(type: ActionType, payload?: P): - Action { - return { type, payload, error: false }; -} - -export function createEmptyAction(type: ActionType): - EmptyAction { - return { type, error: false }; -} - -export function createErrorAction(type: ActionType, payload: P): - Required> { - return { type, payload, error: true }; -} \ No newline at end of file diff --git a/apps/frontend/src/types/LevelTypes.tsx b/apps/frontend/src/types/LevelTypes.tsx deleted file mode 100644 index 6502226c..00000000 --- a/apps/frontend/src/types/LevelTypes.tsx +++ /dev/null @@ -1,34 +0,0 @@ -enum Level { - KINDERGARTEN, - ELEMENTARY_SCHOOL_1, - ELEMENTARY_SCHOOL_2, - MIDDLE_SCHOOL, - HIGH_SCHOOL, - HIGHER_EDUCATION, - RESEARCH -} - -const levelLabel = (level: Level) => { - switch (level) { - case Level.KINDERGARTEN: - return 'levels.kinderGarten'; - case Level.ELEMENTARY_SCHOOL_1: - return 'levels.elementarySchool1'; - case Level.ELEMENTARY_SCHOOL_2: - return 'levels.elementarySchool2'; - case Level.MIDDLE_SCHOOL: - return 'levels.middleSchool'; - case Level.HIGH_SCHOOL: - return 'levels.highSchool'; - case Level.HIGHER_EDUCATION: - return 'levels.higherEducation'; - case Level.RESEARCH: - return 'levels.research'; - default: - return ''; - } -}; - -const levelsCount = Object.keys(Level).length / 2; - -export { Level, levelLabel, levelsCount }; diff --git a/apps/frontend/src/types/ProjectTypes.tsx b/apps/frontend/src/types/ProjectTypes.tsx deleted file mode 100644 index 0d275cd1..00000000 --- a/apps/frontend/src/types/ProjectTypes.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export interface ProjectRouteParams { - projectId: string; -} \ No newline at end of file diff --git a/apps/frontend/src/types/StateTypes.tsx b/apps/frontend/src/types/StateTypes.tsx deleted file mode 100644 index 511e6a6f..00000000 --- a/apps/frontend/src/types/StateTypes.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - AnnotationRecord, - CommentRecord, - Credentials, - ProjectGraphRecord, - SigninErrors, - TagData, - UserRecord, -} from "@celluloid/types"; -import { RouterState } from "connected-react-router"; - -import * as SigninDialog from "~components/Signin"; - -import { PeertubeVideoInfo } from "./YoutubeTypes"; - -export interface SigninState { - loading: boolean; - dialog: SigninDialog.SigninState; - errors: SigninErrors; - credentials?: Credentials; -} - -export interface VideoState { - status: ComponentStatus; - loadingError?: boolean; - annotations: AnnotationRecord[]; - editing: boolean; - commenting: boolean; - annotationError?: string; - focusedAnnotation?: AnnotationRecord; - upsertAnnotationLoading: boolean; - deleteAnnotationLoading: boolean; - commentError?: string; - focusedComment?: CommentRecord; - upsertCommentLoading: boolean; - deleteCommentLoading: boolean; -} - -export interface ProjectDetailsState { - status: ComponentStatus; - error?: string; - project?: ProjectGraphRecord; - setPublicLoading: boolean; - setCollaborativeLoading: boolean; - unshareLoading: boolean; - deleteLoading: boolean; - setPublicError?: string; - setCollaborativeError?: string; - unshareError?: string; - deleteError?: string; -} - -export interface PlayerState { - seeking: boolean; - seekTarget: number; -} - -export interface ProjectState { - player: PlayerState; - video: VideoState; - details: ProjectDetailsState; -} - -export enum SharingStatus { - OPEN, - ERROR, - LOADING, - CLOSED, -} - -export enum ComponentStatus { - LOADING, - ERROR, - READY, -} - -export interface HomeState { - errors: { - projects?: string; - video?: string; - createProject?: string; - }; - projects: ProjectGraphRecord[]; - video?: PeertubeVideoInfo; - createProjectLoading: boolean; -} - -export interface SharingState { - status: SharingStatus; - error?: string; -} - -export interface AppState extends RouterState { - tags: TagData[]; - sharing: SharingState; - project: ProjectState; - home: HomeState; - user?: UserRecord; - signin: SigninState; - updated: boolean; -} diff --git a/apps/frontend/src/types/YoutubeTypes.tsx b/apps/frontend/src/types/YoutubeTypes.tsx deleted file mode 100644 index 12e59826..00000000 --- a/apps/frontend/src/types/YoutubeTypes.tsx +++ /dev/null @@ -1,32 +0,0 @@ -export interface PeertubeVideoInfo { - id: string; - title: string; - thumbnailUrl: string; - host:string; -} - -export interface Player { - getCurrentTime(): number; - getDuration(): number; - playVideo(): void; - pauseVideo(): void; - seekTo(position: number, allowSeekAhead: boolean): void; -} - -export interface PlayerReadyEvent { - target: Player; -} - -export interface PlayerChangeEvent { - target: Player; - data: number; -} - -export enum PlayerEventData { - UNSTARTED = -1, - ENDED = 0, - PLAYING = 1, - PAUSED = 2, - BUFFERING = 3, - CUED = 5 -} \ No newline at end of file diff --git a/apps/frontend/src/services/Constants.ts b/apps/frontend/src/utils/Constants.ts similarity index 100% rename from apps/frontend/src/services/Constants.ts rename to apps/frontend/src/utils/Constants.ts diff --git a/apps/server/package.json b/apps/server/package.json deleted file mode 100644 index ebed9540..00000000 --- a/apps/server/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "server", - "version": "2.0.0", - "description": "Celluloid backend", - "repository": "http://github.com/celluloid-camp/celluloid", - "author": "Younes Benaomar ", - "license": "MIT", - "private": true, - "dependencies": { - "bcryptjs": "^2.4.3", - "body-parser": "^1.18.2", - "change-case": "^4.1.2", - "compression": "^1.7.2", - "connect-redis": "^7.1.0", - "cookie-parser": "^1.4.3", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "express-pino-logger": "^7.0.0", - "express-session": "^1.17.3", - "extend": "^3.0.2", - "js2xmlparser": "^5.0.0", - "knex": "^2.3.0", - "lodash": "^4.17.21", - "mem": "^4.0.0", - "nodemailer": "^6.0.0", - "nodemailer-smtp-transport": "^2.7.4", - "papaparse": "^5.4.1", - "passport": "^0.6.0", - "passport-local": "^1.0.0", - "pg": "^8.8.0", - "pino": "^8.7.0", - "pino-std-serializers": "^6.0.0", - "ramda": "^0.28.0", - "redis": "^4.6.10", - "source-map-support": "^0.5.13", - "tslib": "^2.2.0", - "unfurl.js": "^6.3.1", - "validator": "^13.7.0" - }, - "scripts": { - "build": "tsup", - "dev": "dotenv -e ../../.env -- tsup --watch --silent --onSuccess 'node dist/index.js'", - "start": "node --use-strict dist/index.js" - }, - "devDependencies": { - "@celluloid/config": "*", - "@celluloid/types": "*", - "@celluloid/validators": "*", - "@types/bcrypt": "^3.0.0", - "@types/bcryptjs": "^2.4.2", - "@types/compression": "^1.0.0", - "@types/cookie-parser": "^1.4.1", - "@types/dotenv": "^6.0.0", - "@types/express": "^4.0.39", - "@types/express-pino-logger": "^4.0.2", - "@types/express-session": "^1.15.8", - "@types/knex": "^0.16.1", - "@types/node": "^18.13.0", - "@types/node-fetch": "^2.6.2", - "@types/nodemailer": "^6.4.6", - "@types/nodemailer-smtp-transport": "^2.7.4", - "@types/papaparse": "^5.3.7", - "@types/passport": "^1.0.0", - "@types/passport-local": "^1.0.33", - "@types/pg": "^7.4.1", - "@types/ramda": "^0.25.35", - "dotenv-cli": "^7.2.1", - "jest": "^27.0.1", - "knex-types": "^0.4.0", - "mock-req": "^0.2.0", - "pino-pretty": "^9.1.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" - } -} diff --git a/apps/server/src/Config.ts b/apps/server/src/Config.ts deleted file mode 100644 index 51440f5a..00000000 --- a/apps/server/src/Config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as dotEnv from 'dotenv'; - -import { envFile } from './Paths'; - -dotEnv.config({ path: envFile}); \ No newline at end of file diff --git a/apps/server/src/Paths.ts b/apps/server/src/Paths.ts deleted file mode 100644 index 26d324f6..00000000 --- a/apps/server/src/Paths.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as path from "path"; - -export const rootDir = path.resolve(__dirname, "..", "..", ".."); -export const envFile = path.resolve(rootDir, ".env"); -export const clientDir = path.resolve(rootDir, "apps", "client", "dist"); -export const publicDir = path.resolve(__dirname, "..", "public"); -export const clientApp = path.resolve(clientDir, "index.html"); diff --git a/apps/server/src/api/AnnotationApi.ts b/apps/server/src/api/AnnotationApi.ts deleted file mode 100644 index ab35d7df..00000000 --- a/apps/server/src/api/AnnotationApi.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { AnnotationData, AnnotationRecord, UserRecord } from "@celluloid/types"; -import * as express from "express"; -import Papa from 'papaparse'; - -import { isProjectOwnerOrCollaborativeMember } from "../auth/Utils"; -import { logger } from "../backends/Logger"; -import * as AnnotationStore from "../store/AnnotationStore"; -import * as CommentStore from "../store/CommentStore"; -import * as ProjectStore from "../store/ProjectStore"; -import { convertToSrt } from "../utils/srt"; -import CommentApi from "./CommentApi"; - - -const log = logger("api/AnnotationApi"); -const js2xmlparser = require("js2xmlparser"); - -const router = express.Router({ mergeParams: true }); - -router.use("/:annotationId/comments", CommentApi); - -function fetchComments(annotation: AnnotationRecord, user: UserRecord) { - return CommentStore.selectByAnnotation(annotation.id, user).then((comments) => - Promise.resolve({ ...annotation, comments } as AnnotationRecord) - ); -} - -router.get('/', (req: express.Request<{ projectId: string }>, res: express.Response) => { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - return ProjectStore.selectOne(projectId, user) - .then(() => AnnotationStore.selectByProject(projectId, user)) - .then((annotations: AnnotationRecord[]) => - Promise.all( - annotations.map((annotation) => fetchComments(annotation, user)) - ) - ) - .then((annotations) => { - return res.status(200).json(annotations); - }) - .catch((error: Error) => { - log.error("Failed to list annotations:", error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.get('/export/:format', async (req: express.Request<{ projectId: string, format: string }>, res: express.Response) => { - const projectId = req.params.projectId; - const format = req.params.format; - const user = req.user as UserRecord; - - const annotations = await AnnotationStore.selectByProject(projectId, user); - const data = await Promise.all( - annotations.map((annotation) => fetchComments(annotation, user)) - ); - - const formated = data.map((annotation) => ({ - startTime: annotation.startTime, - endTime: annotation.stopTime, - text: annotation.text, - comments: annotation.comments.map((comment) => comment.text) - })) - - let content = ""; - if (format === 'xml') { - content = js2xmlparser.parse("annotations", formated); - } else if (format == "csv") { - - content = Papa.unparse(formated); - } else if (format == "srt") { - - content = convertToSrt(formated); - - } - - res.setHeader('Content-Disposition', `attachment; filename="data.${format}"`); - res.setHeader('Content-Type', `text/${format}`); - res.send(content); - -}); - - - -router.post("/", isProjectOwnerOrCollaborativeMember, (req, res) => { - const projectId = req.params.projectId; - const annotation = req.body as AnnotationData; - const user = req.user as UserRecord; - - AnnotationStore.insert(annotation, user, projectId) - .then((result) => fetchComments(result, user)) - .then((result) => { - return res.status(201).json(result); - }) - .catch((error: Error) => { - log.error(error, "Failed to create annotation"); - return res.status(500).send(); - }); -}); - -router.put( - "/:annotationId", - isProjectOwnerOrCollaborativeMember, - (req, res) => { - const annotationId = req.params.annotationId; - const updated = req.body; - const user = req.user as UserRecord; - - AnnotationStore.selectOne(annotationId, user) - .then((old: AnnotationRecord) => - old.userId !== user.id - ? Promise.reject(new Error("UserNotAnnotationOwner")) - : Promise.resolve() - ) - .then(() => AnnotationStore.update(annotationId, updated, user)) - .then((result) => fetchComments(result, user)) - .then((result) => res.status(200).json(result)) - .catch((error: Error) => { - log.error("Failed to update annotation:", error); - if (error.message === "AnnotationNotFound") { - return res.status(404).json({ error: error.message }); - } else if (error.message === "UserNotAnnotationOwner") { - return res.status(403).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); - } -); - -router.delete( - "/:annotationId", - isProjectOwnerOrCollaborativeMember, - (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user as UserRecord; - - AnnotationStore.selectOne(annotationId, user) - .then((old: AnnotationRecord) => - old.userId !== user.id - ? Promise.reject(new Error("UserNotAnnotationOwner")) - : Promise.resolve() - ) - .then(() => AnnotationStore.del(annotationId)) - .then(() => res.status(204).send()) - .catch((error: Error) => { - log.error("Failed to delete annotation:", error); - if (error.message === "AnnotationNotFound") { - return res.status(404).json({ error: error.message }); - } else if (error.message === "UserNotAnnotationOwner") { - return res.status(403).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); - } -); - -export default router; diff --git a/apps/server/src/api/CommentApi.ts b/apps/server/src/api/CommentApi.ts deleted file mode 100644 index 60209f7d..00000000 --- a/apps/server/src/api/CommentApi.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { CommentRecord, UserRecord } from '@celluloid/types'; -import * as express from 'express'; - -import { - isLoggedIn, - isProjectOwnerOrCollaborativeMember -} from '../auth/Utils'; -import { logger } from '../backends/Logger'; -import * as AnnotationStore from '../store/AnnotationStore'; -import * as CommentStore from '../store/CommentStore'; - - -const log = logger('api/CommentApi'); - -const router = express.Router({ mergeParams: true }); - -router.get('/', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user; - - AnnotationStore.selectOne(annotationId, user) - .then(() => CommentStore.selectByAnnotation(annotationId, user as UserRecord)) - .then((comments: CommentRecord[]) => - res.status(200).json(comments)) - .catch((error: Error) => { - log.error('Failed to list comments:', error); - if (error.message === 'AnnotationNotFound') { - res.status(404).json({ error: error.message }); - } else { - res.status(500).send(); - } - }); -}); - -router.post('/', isLoggedIn, isProjectOwnerOrCollaborativeMember, async (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user; - const comment = req.body.text; - - try { - await AnnotationStore.selectOne(annotationId, user); - const result = await CommentStore.insert(annotationId, comment, user as UserRecord); - - log.debug(result, "resutl"); - return res.status(201).json(result) - }catch(error){ - if (error.message === 'AnnotationNotFound') { - return res.status(404).json({ error: error.message }); - } - return res.status(500).send(); - - } -}); - -router.put('/:commentId', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req:any, res) => { - const commentId = req.params.commentId; - const updated = req.body; - const user = req.user; - - CommentStore.selectOne(commentId) - .then((old: CommentRecord) => - old.userId !== user.id - ? Promise.reject(new Error('UserNotCommentOwner')) - : Promise.resolve() - ) - .then(() => CommentStore.update(commentId, updated.text)) - .then(result => res.status(200).json(result)) - .catch((error: Error) => { - log.error('Failed to update comment:', error); - if (error.message === 'CommentNotFound') { - return res.status(404).json({ error: error.message }); - } else if (error.message === 'UserNotCommentOwner') { - return res.status(403).send({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.delete('/:commentId', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req:any, res) => { - const commentId = req.params.commentId; - const user = req.user; - - CommentStore.selectOne(commentId) - .then((comment: CommentRecord) => - comment.userId !== user.id - ? Promise.reject(new Error('UserNotCommentOwner')) - : Promise.resolve() - ) - .then(() => CommentStore.del(commentId)) - .then(() => res.status(204).send()) - .catch((error: Error) => { - log.error('Failed to delete comment:', error); - if (error.message === 'CommentNotFound') { - return res.status(404).json({ error: error.message }); - } else if (error.message === 'UserNotCommentOwner') { - return res.status(403).send({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -export default router; \ No newline at end of file diff --git a/apps/server/src/api/ProjectApi.ts b/apps/server/src/api/ProjectApi.ts deleted file mode 100644 index 16f7a419..00000000 --- a/apps/server/src/api/ProjectApi.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { - ProjectCreateData, - ProjectGraphRecord, - ProjectRecord, - UserRecord, -} from "@celluloid/types"; -import { Router } from "express"; - -import { isProjectOwner, isTeacher } from "../auth/Utils"; -import { logger } from "../backends/Logger"; -import * as ProjectStore from "../store/ProjectStore"; -import AnnotationsApi from "./AnnotationApi"; - -const log = logger("api/ProjectApi"); - -const router = Router({ mergeParams: true }); - -router.use("/:projectId/annotations", AnnotationsApi); - -function fetchMembers( - project: ProjectRecord, - user?: Partial -): Promise { - if (project.collaborative || (user && user.id === project.userId)) { - return ProjectStore.selectProjectMembers(project.id); - } else if (user) { - return ProjectStore.isMember(project.id, user).then((member) => - member ? Promise.resolve([user] as UserRecord[]) : Promise.resolve([]) - ); - } else { - return Promise.resolve([]); - } -} - - - -router.get("/:projectId", (req, res) => { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - ProjectStore.selectOne(projectId, user) - .then((project: any) => { - return res.json(project); - }) - .catch((error: Error) => { - console.error(error) - log.error(`Failed to fetch project ${projectId}:`, error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.post("/", isTeacher, (req, res) => { - const user = req.user as UserRecord; - const project = req.body as ProjectCreateData; - - ProjectStore.insert(project, user) - .then((result: any) => { - return res.status(201).json(result); - }) - .catch((error: Error) => { - console.log(error); - log.error(`Failed to create project: ${JSON.stringify(error)}`); - return res.status(500).send(); - }); -}); - -router.put("/:projectId", isTeacher, isProjectOwner, (req: any, res) => { - ProjectStore.update(req.body, req.params.projectId) - .then((result) => res.status(200).json(result)) - .catch((error) => { - log.error("Failed to update project:", error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId", isTeacher, isProjectOwner, (req, res) => { - ProjectStore.del(req.params.projectId) - .then(() => res.status(204).send()) - .catch((error) => { - log.error("Failed to delete project:", error); - return res.status(500).send(); - }); -}); - -router.get("/:projectId/members", (req, res) => { - const projectId = req.params.projectId; - const user = req.user; - ProjectStore.selectOne(projectId, user as UserRecord) - .then((project: any) => fetchMembers(project, req.user)) - .then((members) => res.status(200).json(members)) - .catch((error) => { - log.error("Failed to list project members:", error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.put("/:projectId/share", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.shareById(projectId, req.body) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to share project with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId/share", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.unshareById(projectId) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to unshare project with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.put("/:projectId/public", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.setPublicById(projectId, true) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to set project public with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId/public", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.setPublicById(projectId, false) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset public on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); -}); - -router.put( - "/:projectId/collaborative", - isTeacher, - isProjectOwner, - (req, res) => { - const projectId = req.params.projectId; - return ProjectStore.setCollaborativeById(projectId, true) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset collaborative on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); - } -); - -router.delete( - "/:projectId/collaborative", - isTeacher, - isProjectOwner, - (req, res) => { - const projectId = req.params.projectId; - return ProjectStore.setCollaborativeById(projectId, false) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset collaborative on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); - } -); - -export default router; diff --git a/apps/server/src/api/TagApi.ts b/apps/server/src/api/TagApi.ts deleted file mode 100644 index 195af3a5..00000000 --- a/apps/server/src/api/TagApi.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as express from 'express'; - -import { isTeacher } from '../auth/Utils'; -import { logger } from '../backends/Logger'; -import * as TagStore from '../store/TagStore'; - -const log = logger('api/Tag'); - -const router = express.Router(); - -router.get('/', (_, res) => { - TagStore.selectAll() - .then(result => res.status(200).json(result)) - .catch(error => { - log.error('Failed to fetch tags:', error); - return res.status(500).send(); - }); -}); - -router.post('/', isTeacher, (req, res) => { - const { name } = req.body; - return TagStore.insert(name) - .then(result => - res.status(201).json(result) - ) - .catch(error => { - log.error('Failed to add new tag:', error); - return res.status(500).send(); - }); -}); - -export default router; diff --git a/apps/server/src/api/UnfurlApi.ts b/apps/server/src/api/UnfurlApi.ts deleted file mode 100644 index cdc08241..00000000 --- a/apps/server/src/api/UnfurlApi.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as express from 'express'; -import { unfurl } from 'unfurl.js' -import { URL } from 'url'; - -import { isLoggedIn } from '../auth/Utils'; -import { logger } from '../backends/Logger'; - -const log = logger('api/UnfulApi'); - -const router = express.Router(); - -type Result = { - faviconUrl: string | undefined - website: string | undefined - imageUrl: string | undefined - title: string | undefined - description: string | undefined -}; - -router.get('/', isLoggedIn, async (req, res) => { - const url = req.query.url as string; - try { - const raw = await unfurl(url); - const parsedUrl = new URL(url as string); - const result: Result = { - faviconUrl: "", - website: "", - imageUrl: undefined, - title: undefined, - description: undefined, - }; - - const ogp = raw.open_graph; - result.website = ogp.url; - - result.title = ogp.title || raw.description; - result.description = ogp.description || raw.description; - result.faviconUrl = raw.favicon - - result.imageUrl = - ogp.images && ogp.images.length > 0 ? - ogp.images[0].url : - undefined; - if (result.title && result.description) { - if (!result.website) { - result.website = parsedUrl.hostname; - } - } - return res.status(200).json(result); - - } catch (e) { - log.error(`could not unfurl link: ${e.message}`); - return res.status(500); - } - - -}); - -export default router; diff --git a/apps/server/src/api/UserApi.ts b/apps/server/src/api/UserApi.ts deleted file mode 100644 index b6939c64..00000000 --- a/apps/server/src/api/UserApi.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import { - validateConfirmResetPassword, - validateConfirmSignup, - validateLogin, - validateSignup, - validateStudentSignup, -} from "@celluloid/validators"; -import { Request, Response, Router } from "express"; -import passport from "passport"; - -import { SigninStrategy } from "../auth/Auth"; -import { - isLoggedIn, - sendConfirmationCode, - sendPasswordReset, -} from "../auth/Utils"; -import { hasConflictedOn } from "../backends/Database"; -import { logger } from "../backends/Logger"; -import * as UserStore from "../store/UserStore"; -import { TeacherServerRecord } from "../types/UserTypes"; - -const log = logger("api/User"); - -const router = Router(); - -router.post("/student-signup", (req, res, next) => { - const payload = req.body; - const result = validateStudentSignup(payload); - - - if (!result.success) { - log.error( - `Failed student signup with data ${payload}: bad request:`, - result - ); - return res.status(400).json(result); - } - return passport.authenticate(SigninStrategy.STUDENT_SIGNUP, (error: Error) => { - if (error) { - console.log("error", error) - log.error( - `Failed student signup with username ${payload.username}:`, - error - ); - if (hasConflictedOn(error, "User", "username")) { - return res.status(409).json({ - success: false, - errors: { username: "UsernameAlreadyTaken" }, - }); - } else if (error.message === "IncorrectProjectPassword") { - return res.status(403).send(); - } else { - return res.status(500).send(); - } - } else { - log.info( - `New signup for student with username ${payload.username}`, - result - ); - return res.status(201).json(result); - } - })(req, res, next); -}); - -router.post("/signup", (req, res, next) => { - const payload = req.body; - const result = validateSignup(payload); - - if (!result.success) { - log.error(`Failed user signup with data ${payload}: bad request:`, result); - return res.status(400).json(result); - } - - return passport.authenticate(SigninStrategy.TEACHER_SIGNUP, (error: Error) => { - if (error) { - log.error(`Failed user signup with email ${payload.email}:`, error); - if (hasConflictedOn(error, "User", "username")) { - return res.status(409).json({ - success: false, - errors: { username: "UsernameAlreadyTaken" }, - }); - } else if (hasConflictedOn(error, "User", "email")) { - return res.status(409).json({ - success: false, - errors: { email: "EmailAlreadyTaken" }, - }); - } else { - return res.status(500).send(); - } - } else { - log.info(`New signup from teacher with email ${payload.email}`, result); - return res.status(201).json(result); - } - })(req, res, next); -}); - -router.post("/login", (req, res, next) => { - const payload = req.body; - const result = validateLogin(req.body); - - if (!result.success) { - log.error( - `Failed user login with data ${JSON.stringify(payload)}: bad request:`, - result - ); - return res.status(400).json(result); - } - return passport.authenticate(SigninStrategy.LOGIN, (error: Error, user: Express.User) => { - if (error) { - log.error(`Failed user login with data ${payload}:`, error); - return res.status(401).json({ - success: false, - errors: { server: error.message }, - }); - } else { - return req.login(user, (err) => { - if (err) { - return res.status(500).send(); - } else { - return res.status(200).json(result); - } - }); - } - })(req, res, next); -}); - -function compareCodes(expected: string, actual: string) { - return expected.replace(/\s/g, "") === actual.replace(/\s/g, ""); -} - -router.post("/confirm-signup", (req, res) => { - const payload: any = req.body; - const result = validateConfirmSignup(payload); - - if (!result.success) { - return res.status(400).json(result); - } - return UserStore.selectOneByUsernameOrEmail(payload.login) - .then((user: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to confirm signup: user` + - ` with email ${payload.login} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - if (compareCodes(user.code || "", payload.code)) { - return UserStore.confirmByEmail(payload.login) - .then(() => res.status(200).json(result)) - .catch((error: Error) => { - log.error( - `Failed to confirm signup for user` + - ` with email ${payload.login}:`, - error - ); - return res.status(500).send(); - }); - } else { - log.error( - `Failed to confirm signup for user with email` + - ` ${payload.login}: received code ${payload.code}, expected ${user.code}` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } - } - }) - .catch((error) => { - log.error(`Failed to confirm signup:`, error); - return res.status(500).send(); - }); -}); - -router.post("/confirm-reset-password", (req, res) => { - const payload: any = req.body; - const result = validateConfirmResetPassword(payload); - - if (!result.success) { - return res.status(400).json(result); - } - return UserStore.selectOneByUsernameOrEmail(payload.login) - .then((user?: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to confirm password reset: user with email ${payload.login} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - if (compareCodes(user.code || "", payload.code)) { - return UserStore.updatePasswordByEmail( - payload.login.trim(), - payload.password - ) - .then(() => res.status(200).json(result)) - .catch((error: Error) => { - log.error( - `Failed to confirm password reset for user with email ${payload.login}`, - error - ); - return res.status(500).send(); - }); - } else { - log.error( - `Failed to confirm password reset for user with email ${payload.login}:` + - ` received code ${payload.code}, expected ${user.code}` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } - } - }) - .catch((error) => { - log.error(`Failed to confirm password reset:`, error); - return res.status(500).send(); - }); -}); - -const resendCode = - (sender: (user: TeacherRecord) => Promise) => - (req: Request, res: Response) => { - const payload = req.body; - - if (!payload.email || payload.email.trim().length === 0) { - return res.status(400).json({ - success: false, - errors: { email: "MissingEmail" }, - }); - } - return UserStore.selectOneByUsernameOrEmail(payload.email) - .then((user?: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to resend authorization code:` + - ` user with email ${payload.email} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - return UserStore.updateCodeByEmail(payload.email).then( - (updatedUser: TeacherRecord) => - sender(updatedUser).then(() => - res.status(200).json({ success: true, errors: {} }) - ) - ); - } - }) - .catch((error: Error) => { - log.error( - `Failed to resend authorization code for user ` + - ` with email ${payload.email}`, - error - ); - return res.status(500).send(); - }); - }; - -router.post("/reset-password", (req, res) => { - return resendCode(sendPasswordReset)(req, res); -}); - -router.post("/resend-code", (req, res) => { - return resendCode(sendConfirmationCode)(req, res); -}); - -router.get("/me", isLoggedIn, (req: any, res) => { - if (req.user) { - return res.status(200).json({ - // compatibility with old frontend - teacher: { - username: req.user.username, - id: req.user.id, - role: req.user.role, - }, - username: req.user.username, - id: req.user.id, - role: req.user.role, - email: req.user.email, - }); - } else { - return res.status(401).send(); - } -}); - -router.put("/logout", isLoggedIn, (req, res) => { - if (req.session) { - req.session.destroy((err) => { - if (err) { - res.status(400).send("Unable to log out"); - } else { - res.send("Logout successful"); - } - }); - } else { - res.end(); - } -}); - -export default router; diff --git a/apps/server/src/api/VideoApi.ts b/apps/server/src/api/VideoApi.ts deleted file mode 100644 index 086bc87a..00000000 --- a/apps/server/src/api/VideoApi.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { PeerTubeVideo } from "@celluloid/types"; -import * as express from "express"; -import fetch from "node-fetch"; -import { last } from "ramda"; -import { URL } from "url"; - -import { logger } from "../backends/Logger"; - -const log = logger("api/videoApi"); - -const router = express.Router(); - -type PeerTubeVideoInfo ={ - id: string; - title: string; - thumbnailUrl: string; - host:string; -} - -async function getPeerVideoInfo(videoUrl: string): Promise { - var parsed = new URL(videoUrl); - - const host = parsed.host; - const videoId = last(parsed.pathname.split("/")); - - const url = `https://${host}/api/v1/videos/${videoId}`; - - try { - const response = await fetch(url, { - method: "GET", - headers: { - Accepts: "application/json", - }, - }); - - if (response.status === 200) { - const data:PeerTubeVideo = await response.json(); - return { - id: data.shortUUID, - host, - title: data.name, - thumbnailUrl: `https://${host}/${data.thumbnailPath}` - }; - } - log.error( - `Could not perform PeerTube API request (error ${response.status})` - ); - throw new Error("Could not perform PeerTube API request "); - } catch (e: any) { - throw new Error("Could not perform PeerTube API request "); - } -} - -router.get("/", async (req, res) => { - if (req.query.url) { - try { - const data = await getPeerVideoInfo(req.query.url as string); - return res.status(200).json(data); - } catch (e: any) { - return res.status(500); - } - } - return res.status(500); -}); - -export default router; diff --git a/apps/server/src/auth/Auth.ts b/apps/server/src/auth/Auth.ts deleted file mode 100644 index 883b6110..00000000 --- a/apps/server/src/auth/Auth.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import bcrypt from 'bcryptjs'; -import passport from "passport"; -import { - Strategy, - VerifyFunction, - VerifyFunctionWithRequest, -} from "passport-local"; - -import { logger } from "../backends/Logger"; -import * as ProjectStore from "../store/ProjectStore"; -import * as UserStore from "../store/UserStore"; -import { TeacherServerRecord, UserServerRecord } from "../types/UserTypes"; -import { sendConfirmationCode } from "./Utils"; - -const log = logger("auth/Auth"); - - -declare global { - namespace Express { - interface User { - id: string; - } - } -} - -export enum SigninStrategy { - LOGIN = "login", - TEACHER_SIGNUP = "teacher-signup", - STUDENT_SIGNUP = "student-signup", -} - -passport.serializeUser((user, done) => { - return Promise.resolve(done(null, user.id)); -}); -passport.deserializeUser((id: string, done: any) => { - return UserStore.selectOne(id) - .then((result: TeacherRecord) => { - if (result) { - return Promise.resolve(done(null, result)); - } else { - log.error( - `Deserialize user failed: user with id` + ` ${id} does not exist` - ); - return Promise.resolve(done(new Error("InvalidUser"))); - } - }) - .catch((error: Error) => Promise.resolve(done(error))); -}); - -const signStudentUp: VerifyFunctionWithRequest = ( - req, - username, - password, - done -) => { - const { shareCode } = req.body; - - return ProjectStore.selectOneByShareName(shareCode) - .then((result) => { - if (result) { - return UserStore.createStudent(username, password, result.id); - return Promise.reject(new Error("IncorrectProjectPassword")); - } - }) - .then((user: any) => Promise.resolve(done(null, user))) - .catch((error: Error) => { - log.error("Failed to signup student:", error); - return Promise.resolve(done(error)); - }); -}; - -const signTeacherUp: VerifyFunctionWithRequest = ( - req, - email, - password, - done -) => { - return UserStore.createTeacher(req.body.username, email, password) - .then((user: TeacherServerRecord) => sendConfirmationCode(user)) - .then((user: TeacherRecord) => Promise.resolve(done(null, user))) - .catch((error: Error) => Promise.resolve(done(error))); -}; - -const logUserIn: VerifyFunction = (login, password, done) => { - return UserStore.selectOneByUsernameOrEmail(login) - .then((user: UserServerRecord) => { - if (!user) { - return Promise.resolve(done(new Error("InvalidUser"))); - } - if (!bcrypt.compareSync(password, user.password)) { - log.error(`Login failed for user ${user.username}: incorrect password`); - return Promise.resolve(done(new Error("InvalidUser"))); - } - if (!user.confirmed && user.role !== "Student") { - log.error(`Login failed: ${user.username} is not confirmed`); - return Promise.resolve(done(new Error("UserNotConfirmed"))); - } - return Promise.resolve(done(null, user)); - }) - .catch((error: Error) => Promise.resolve(done(error))); -}; - -export const loginStrategy = new Strategy( - { usernameField: "login" }, - logUserIn -); - -export const teacherSignupStrategy = new Strategy( - { passReqToCallback: true, usernameField: "email" }, - signTeacherUp -); - -export const studentSignupStrategy = new Strategy( - { passReqToCallback: true, usernameField: "username" }, - signStudentUp -); diff --git a/apps/server/src/auth/Utils.ts b/apps/server/src/auth/Utils.ts deleted file mode 100644 index 8dd7c893..00000000 --- a/apps/server/src/auth/Utils.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { UserRecord } from '@celluloid/types'; -import bcrypt from 'bcryptjs'; -import { paramCase } from 'change-case'; -import { NextFunction, Request, Response } from 'express'; - -import { sendMail } from '../backends/Email'; -import { logger } from '../backends/Logger'; -import * as ProjectStore from '../store/ProjectStore'; -import { TeacherServerRecord } from '../types/UserTypes'; - -const log = logger('auth/Auth'); - -export function hashPassword(password: string) { - const salt = bcrypt.genSaltSync(); - return bcrypt.hashSync(password, salt); -} - -export function isLoggedIn( - req: Request, - res: Response, - next: NextFunction) { - if (!req.user) { - return Promise.resolve(res.status(401).json({ - error: 'LoginRequired' - })); - } - return Promise.resolve(next()); -} - -export function isTeacher( - req: any, - res: Response, - next: NextFunction) { - if ((!req.user || req.user.role !== 'Teacher') && (!req.user || req.user.role !== 'Admin')) { - log.error('User is must be a teacher'); - return Promise.resolve(res.status(403).json({ - error: 'TeacherRoleRequired' - })); - } - return Promise.resolve(next()); -} - -export function isProjectOwner( - req: Request, - res: Response, - next: NextFunction) { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - return ProjectStore.isOwner(projectId, user) - .then((result: boolean) => { - if (result) { - next(); - return Promise.resolve(); - } - log.error('User must be project owner'); - res.status(403).json({ - error: 'ProjectOwnershipRequired' - }); - return Promise.resolve(); - }) - .catch(error => { - log.error('Failed to check project ownership:', error); - return Promise.resolve(res.status(500).send()); - }); -} - -export function isProjectOwnerOrCollaborativeMember( - req: Request, - res: Response, - next: NextFunction) { - - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - ProjectStore.isOwnerOrCollaborativeMember(projectId, user) - .then((result: boolean) => { - if (result) { - next(); - return Promise.resolve(); - } - log.error('User must be project owner or collaborator'); - res.status(403).json({ - error: 'ProjectOwnershipOrMembershipRequired' - }); - return Promise.resolve(); - }) - .catch(error => { - log.error('Failed project ownership/membership test:', error); - return Promise.resolve(res.status(500).send()); - }); -} - -export function generateConfirmationCode() { - const code = () => String(Math.floor(Math.random() * 900) + 100); - const first = code(); - const second = code(); - return `${first}${second}`; -} - -export function generateUniqueShareName(title: string, count: number) { - const compare = (a: string, b: string) => - b.length - a.length; - - const construct = (result: string[], str: string) => { - let res: string[] = [] - if (str) { - if (result.join().length < 6) { - res = [...result, str]; - } - } - return res; - }; - - const prefix = paramCase(title) - .split(/-/) - .sort(compare) - .reduce(construct, []) - .join('-'); - - return `${prefix}${count ? count : ''}`; -} - -export function sendConfirmationCode(user: TeacherServerRecord) { - const subject = `Bienvenue sur Celluloid, ${user.username} !`; - const text = - `Bonjour ${user.username},\n\n` + - `Voici votre code de confirmation : ${user.code}\n\n` + - `Ce code est valable pendant 1 heure.\n\n` + - `Veuillez le saisir dans le formulaire prévu à cet effet.\n\n` + - `L'équipe Celluloid vous souhaite la bienvenue !`; - const html = - `Bonjour ${user.username},` + - `Voici votre code de confirmation : ${user.code}` + - `Ce code est valable pendant 1 heure.` + - `Veuillez le saisir dans le formulaire prévu à cet effet.` + - `L'équipe Celluloid vous souhaite la bienvenue !`; - - return sendMail(user.email, subject, text, html).then(() => - Promise.resolve(user) - ); -} - -export function sendPasswordReset(user: TeacherServerRecord) { - const subject = `${user.username - } : réinitialisation de votre mot de passe Celluloid`; - const text = - `Bonjour ${user.username},\n\n` + - `Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}\n\n` + - `Voici votre code de confirmation: ${user.code}\n\n` + - `Ce code sera valable pendant 1 heure.\n\n` + - `Veuillez le saisir dans le formulaire prévu à cet effet.\n\n` + - `Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.\n\n` + - `Cordialement,\n\n` + - `L'équipe Celluloid`; - const html = - `Bonjour ${user.username},` + - `Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}` + - `Voici votre code de confirmation : ${user.code}` + - `Ce code sera valable pendant 1 heure.` + - `Veuillez le saisir dans le formulaire prévu à cet effet.` + - `Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.` + - `Cordialement,` + - `L'équipe Celluloid`; - - return sendMail(user.email, subject, text, html).then(() => - Promise.resolve(user) - ); -} diff --git a/apps/server/src/backends/Database.ts b/apps/server/src/backends/Database.ts deleted file mode 100644 index bf970f7e..00000000 --- a/apps/server/src/backends/Database.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Knex from "knex"; -import * as R from "ramda"; - -import configuration from "../knexfile" -import { logger } from "./Logger"; - -const log = logger("Database"); - -export const database = Knex(configuration) - - -export const filterNull = - (prop: string) => - // tslint:disable-next-line:no-any - (obj: any) => { - // tslint:disable-next-line:no-any - obj[prop] = obj[prop].filter((elem: any) => elem); - return obj; - }; - -export function getExactlyOne(rows: any[]) { - if (rows.length === 1) { - return Promise.resolve(rows[0]); - } else { - log.error("Update or insert result has less or more than one row", rows); - return Promise.reject(Error("NotExactlyOneRow")); - } -} - -const CONFLICT_ERROR = "23505"; - -interface DatabaseError extends Error { - code?: string; - constraint?: string; -} - -export function hasConflictedOn( - error: DatabaseError, - table: string, - key: string -) { - return ( - error.code && - error.constraint && - error.code === CONFLICT_ERROR && - R.equals(error.constraint.split("_"), [table, key, "key"]) - ); -} diff --git a/apps/server/src/backends/Email.ts b/apps/server/src/backends/Email.ts deleted file mode 100644 index 07068faf..00000000 --- a/apps/server/src/backends/Email.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as mailer from 'nodemailer'; -import smtp from 'nodemailer-smtp-transport'; - -import { logger } from './Logger'; - -const log = logger('Email'); - -const transport = mailer.createTransport(smtp({ - host: process.env.CELLULOID_SMTP_HOST, - port: parseInt(process.env.CELLULOID_SMTP_PORT || "465", 10), - secure: process.env.CELLULOID_SMTP_SECURE === 'true', - -})); - -export function sendMail( - to: string, subject: string, text: string, html: string) { - const mailOptions = { - from: 'Celluloid ', to, subject, text, html - }; - - return new Promise((resolve, reject) => { - transport.sendMail(mailOptions, (error, info) => { - if (error) { - log.error( - `Failed to send email to ${to} with body [${text}]`, error); - // reject(new Error('Email sending failed')); - resolve(null); - } else { - log.info( - `Email sent to ${to} with subject [${subject}]`, info.response); - resolve(null); - } - }); - }); -} diff --git a/apps/server/src/backends/Logger.ts b/apps/server/src/backends/Logger.ts deleted file mode 100644 index 25312d0f..00000000 --- a/apps/server/src/backends/Logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import pino from 'pino'; - -export const Logger = pino({ - level: process.env.CELLULOID_LOG_LEVEL || 'info' -}); - -export const logger = (module: string) => Logger.child({ module }); \ No newline at end of file diff --git a/apps/server/src/database/connection.ts b/apps/server/src/database/connection.ts deleted file mode 100644 index 392269ed..00000000 --- a/apps/server/src/database/connection.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Knex from "knex"; -import configuration from "../knexfile"; -export const knex = Knex(configuration); \ No newline at end of file diff --git a/apps/server/src/http/SessionStore.ts b/apps/server/src/http/SessionStore.ts deleted file mode 100644 index 4038cb12..00000000 --- a/apps/server/src/http/SessionStore.ts +++ /dev/null @@ -1,34 +0,0 @@ -import RedisStore from "connect-redis" -import session from "express-session"; -import { createClient } from "redis"; - -import { logger } from "../backends/Logger"; - -const log = logger("http/Session"); - -export function createSession() { - const redisClient = createClient({ url: process.env.REDIS_URL || "redis://localhost" }); - redisClient.connect().catch((e) => log.error(`redis error : ${e.message}`)); - - const redisStore = new RedisStore({ - client: redisClient, - }) - log.info("redis connected"); - return session({ - store: redisStore, - name: process.env.CELLULOID_COOKIE_NAME - ? process.env.CELLULOID_COOKIE_NAME - : undefined, - cookie: { - domain: process.env.CELLULOID_COOKIE_DOMAIN - ? process.env.CELLULOID_COOKIE_DOMAIN - : undefined, - secure: process.env.CELLULOID_COOKIE_SECURE === "true", - maxAge: 30 * 24 * 3600 * 1000, - httpOnly: true, - }, - secret: process.env.CELLULOID_COOKIE_SECRET as string, - resave: false, - saveUninitialized: true, - }); -} diff --git a/apps/server/src/index.d.ts b/apps/server/src/index.d.ts deleted file mode 100644 index ca88d8ee..00000000 --- a/apps/server/src/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare global { - namespace Express { - interface User { - id: string; - } - - // These open interfaces may be extended in an application-specific manner via declaration merging. - // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts) - interface Request { - user?: { - id?: string; - }; - } - interface Response {} - interface Application {} - } -} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts deleted file mode 100644 index 4834fbe9..00000000 --- a/apps/server/src/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import "./Config"; - -import bodyParser from "body-parser"; -import compression from "compression"; -import express from "express"; -// import expressPino from "express-pino-logger"; -import passport from "passport"; - -import ProjectsApi from "./api/ProjectApi"; -import TagsApi from "./api/TagApi"; -import UnfurlApi from "./api/UnfurlApi"; -import UsersApi from "./api/UserApi"; -import VideosApi from "./api/VideoApi"; -import { - loginStrategy, - SigninStrategy, - studentSignupStrategy, - teacherSignupStrategy, -} from "./auth/Auth"; -import { logger } from "./backends/Logger"; -import { createSession } from "./http/SessionStore"; -const packageJson = require('../package.json'); - -require("cookie-parser"); - -const log = logger("http"); - -passport.use(SigninStrategy.LOGIN, loginStrategy); -passport.use(SigninStrategy.TEACHER_SIGNUP, teacherSignupStrategy); -passport.use(SigninStrategy.STUDENT_SIGNUP, studentSignupStrategy); -const app = express(); -app.enable('trust proxy'); -// app.use(express.static(publicDir)); -// app.use(express.static(clientDir)); -app.use(bodyParser.json()); -app.use(compression()); -app.use(createSession()); -// app.use(expressPino({ logger: log })); -app.use(passport.initialize()); -app.use(passport.session()); - -app.get("/api/status", (_, res) => res.status(200).json({ - commit: process.env.COMMIT, - version: packageJson.version -})); - -app.use("/api/projects", ProjectsApi); -app.use("/api/users", UsersApi); -app.use("/api/tags", TagsApi); -app.use("/api/unfurl", UnfurlApi); -app.use("/api/video", VideosApi); - - -// app.get("/*", (_, res) => res.sendFile(clientApp)); - - -(async () => { - try { - app.listen(process.env.CELLULOID_LISTEN_PORT, () => { - log.info( - `HTTP server listening on port ${process.env.CELLULOID_LISTEN_PORT}` + - ` in ${process.env.NODE_ENV} mode` - ); - }); - } catch (err) { - log.error(err); - } -})(); diff --git a/apps/server/src/knex/index.ts b/apps/server/src/knex/index.ts deleted file mode 100644 index 1976daeb..00000000 --- a/apps/server/src/knex/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from "./types"; - -import { Knex } from "knex"; -import { Annotation, Comment, Project, User } from "./types"; - -declare module "knex/types/tables" { - interface Tables { - // This is same as specifying `knex('users')` - users: User; - annotations: Annotation; - comments: Comment; - projects: Project; - } -} diff --git a/apps/server/src/knex/types.ts b/apps/server/src/knex/types.ts deleted file mode 100644 index 6b0fb885..00000000 --- a/apps/server/src/knex/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -// The TypeScript definitions below are automatically generated. -// Do not touch them, or risk, your modifications being lost. - -export enum UserRole { - Admin = "Admin", - Teacher = "Teacher", - Student = "Student", -} - -export enum Table { - Annotation = "Annotation", - Comment = "Comment", - Language = "Language", - Project = "Project", - Session = "Session", - Tag = "Tag", - TagToProject = "TagToProject", - User = "User", - UserToProject = "UserToProject", -} - -export type Annotation = { - id: string; - text: string; - startTime: number; - stopTime: number; - pause: boolean; - userId: string; - projectId: string; -}; - -export type Comment = { - id: string; - annotationId: string; - userId: string; - text: string; - createdAt: Date; -}; - -export type Language = { - id: string; - name: string | null; -}; - -export type Project = { - id: string; - videoId: string; - title: string; - description: string; - assignments: string[] | null; - publishedAt: Date; - objective: string; - levelStart: number; - levelEnd: number; - public: boolean; - collaborative: boolean; - userId: string; - shared: boolean; - shareName: string | null; - shareExpiresAt: Date | null; - sharePassword: string | null; - host: string; -}; - -export type Session = { - sid: string; - session: string; - expiresAt: Date; -}; - -export type Tag = { - id: string; - name: string; - featured: boolean; -}; - -export type TagToProject = { - tagId: string; - projectId: string; -}; - -export type User = { - id: string; - email: string | null; - password: string; - confirmed: boolean; - code: string | null; - codeGeneratedAt: Date | null; - username: string; - role: UserRole; -}; - -export type UserToProject = { - userId: string; - projectId: string; -}; - diff --git a/apps/server/src/knexfile.ts b/apps/server/src/knexfile.ts deleted file mode 100644 index a351a9fd..00000000 --- a/apps/server/src/knexfile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as dotEnv from 'dotenv'; -import { Knex } from 'knex'; - -dotEnv.config({ path: '../../.env' }); - - -const configuration = { - client: "pg", - connection: { - host: process.env.CELLULOID_PG_HOST, - database: process.env.CELLULOID_PG_DATABASE, - user: process.env.CELLULOID_PG_USER, - password: process.env.CELLULOID_PG_PASSWORD - }, - pool: { - min: 2, - max: 10 - } -} as Knex.Config - -export default configuration; diff --git a/apps/server/src/middleware/installDatabase.ts b/apps/server/src/middleware/installDatabase.ts deleted file mode 100644 index 0922a505..00000000 --- a/apps/server/src/middleware/installDatabase.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import { Express } from "express"; -import Knex from "knex"; -import configuration from "../knexfile"; -export const knex = Knex(configuration); - -export default (app: Express) => { - - app.set("knex", knex); - - // const shutdownActions = getShutdownActions(app); - // shutdownActions.push(() => { - // rootPgPool.end(); - // }); - - }; \ No newline at end of file diff --git a/apps/server/src/migrations/20221107103108_initial.ts b/apps/server/src/migrations/20221107103108_initial.ts deleted file mode 100644 index 7dc38604..00000000 --- a/apps/server/src/migrations/20221107103108_initial.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.raw('CREATE EXTENSION IF NOT EXISTS "pgcrypto"'); - await knex.raw('CREATE EXTENSION IF NOT EXISTS "plpgsql"'); - await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); - - if (!(await knex.schema.hasTable("User"))) { - await knex.schema.createTable("User", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.string("email").notNullable().unique(); - table.string("username").notNullable().unique(); - table.string("password").notNullable(); - table.boolean("confirmed").notNullable().defaultTo(false); - table.text("code"); - table.timestamp("codeGeneratedAt"); - table - .enu("role", ["Admin", "Teacher", "Student"], { - useNative: true, - enumName: "UserRole", - }) - .checkIn(["Teacher", "Admin"]); - table.jsonb("extra").defaultTo({}); - }); - - knex.schema.raw(` - ALTER TABLE - User - ADD CONSTRAINT - Project_check_userValid - CHECK - ((((role = ANY (ARRAY['Teacher'::public."UserRole", 'Admin'::public."UserRole"])) AND (email IS NOT NULL)) OR ((role = 'Student'::public."UserRole")))) - `); - } - - await knex.schema.createTable("Language", (table) => { - table.text("id").notNullable().unique(); - table.text("name").notNullable(); - }); - - await knex.schema.createTable("Project", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("videoId").notNullable(); - - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - - table.text("title").notNullable(); - table.text("description").notNullable(); - table.text("host"); - table.specificType("assignments", "text[]"); - table.timestamp("publishedAt").notNullable().defaultTo(knex.fn.now()); - table.text("objective").notNullable(); - table.integer("levelStart").notNullable(); - table.integer("levelEnd").notNullable(); - table.boolean("public").notNullable().defaultTo(false); - table.boolean("collaborative").notNullable(); - table.boolean("shared").notNullable().defaultTo(false); - table.text("shareName").unique(); - table.timestamp("shareExpiresAt"), table.text("sharePassword"); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Annotation", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - - table.text("text").notNullable(); - table.float("startTime").notNullable(); - table.float("stopTime").notNullable(); - table.boolean("pause").notNullable(); - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - table - .uuid("projectId") - .notNullable() - .references("Project.id") - .onDelete("CASCADE"); - table.timestamp("createdAt").defaultTo(knex.fn.now()); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Comment", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("text").notNullable(); - table - .uuid("annotationId") - .notNullable() - .references("Annotation.id") - .onDelete("CASCADE"); - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - table.timestamp("createdAt").defaultTo(knex.fn.now()); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Tag", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("name").notNullable(); - table.boolean("featured").notNullable().defaultTo(false); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("TagToProject", (table) => { - table.uuid("tagId").notNullable().references("Tag.id").onDelete("CASCADE"); - table - .uuid("projectId") - .notNullable() - .references("Project.id") - .onDelete("CASCADE"); - table.unique(["tagId", "projectId"], { - indexName: "TagToProjectTagIdProjectIdUnique", - }); - }); - - await knex.schema.createTable("UserToProject", (table) => { - table.uuid("userId").references("User.id").onDelete("CASCADE"); - table.uuid("projectId").references("Project.id").onDelete("CASCADE"); - }); - // } -} - -exports.down = function (knex: Knex): Promise { - throw new Error("Enable to rollback, please use backup"); -}; diff --git a/apps/server/src/migrations/20230117091650_fix-role.ts b/apps/server/src/migrations/20230117091650_fix-role.ts deleted file mode 100644 index 14197715..00000000 --- a/apps/server/src/migrations/20230117091650_fix-role.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.raw( - `ALTER TABLE public."User" DROP CONSTRAINT IF EXISTS "User_role_check"; - ALTER TABLE public."User" ALTER COLUMN email DROP NOT NULL;` - ); -} - -export async function down(): Promise { - return; -} diff --git a/apps/server/src/seeds/default_tags.ts b/apps/server/src/seeds/default_tags.ts deleted file mode 100644 index 4b23c4a1..00000000 --- a/apps/server/src/seeds/default_tags.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Knex } from "knex"; - -const records = [ - { - id: "58d5d8e2-63fd-4f71-993a-642537afe905", - name: "Français - Lettres", - featured: true, - }, - { - id: "b00273d0-f65e-4115-807b-4d6eff189b43", - name: "Mathématiques", - featured: true, - }, - { - id: "f908ead8-f15c-4f5f-84c3-4dfb7e31f1d3", - name: "Histoire", - featured: true, - }, - { - id: "05639d30-37e5-4280-8bcb-092f39c28819", - name: "Géographie", - featured: true, - }, - { - id: "2dc18987-a44f-4b5f-9c0d-be81042b767b", - name: "Technologie", - featured: true, - }, - { - id: "c64c3545-096d-4cb6-9df8-4600aac715bc", - name: "Ëducation civique", - featured: true, - }, - { - id: "553f4da0-5f1d-4ec0-aafb-7dc8adb109e6", - name: "Sciences Physiques", - featured: true, - }, - { - id: "45cf959a-69c5-451a-b185-ef16f2344d7d", - name: "Sport", - featured: true, - }, - { - id: "27eba991-e805-4a82-8281-618b1236380d", - name: "Sciences de la Vie", - featured: true, - }, - { - id: "5a93968a-9047-4d80-a601-539e8393a4cb", - name: "Langues", - featured: true, - }, - { - id: "67b3121e-6893-4ed3-b2df-5318e9bfda5c", - name: "Musique", - featured: true, - }, - { - id: "fbd709d6-68ff-4540-800e-8649bec88892", - name: "Arts", - featured: true, - }, - { - id: "4fa36e4b-b9ea-42ee-8c7a-cf27fa7292eb", - name: "Economie", - featured: true, - }, - { - id: "fefba9b8-32c4-41a7-a3ec-1d810b42d843", - name: "Philosophie", - featured: true, - }, - { - id: "e61332fa-44e5-4719-9e8a-3a62848c44dd", - name: "Projets de recherche", - featured: true, - }, -]; - -export async function seed(knex: Knex): Promise { - await knex.transaction((trx) => { - return trx("Tag").insert(records).onConflict("id").merge(["name", "featured"]) - }); -} diff --git a/apps/server/src/store/AnnotationStore.ts b/apps/server/src/store/AnnotationStore.ts deleted file mode 100644 index 131277da..00000000 --- a/apps/server/src/store/AnnotationStore.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { AnnotationData, AnnotationRecord, UserRecord } from "@celluloid/types"; -import { Knex } from "knex"; - -import { database, getExactlyOne } from "../backends/Database"; -import * as ProjectStore from "./ProjectStore"; - -export function selectByProject(projectId: string, user?: UserRecord) { - return database - .select( - database.raw('"Annotation".*'), - database.raw( - "json_build_object(" + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"' - ) - ) - .from("Annotation") - .innerJoin("Project", "Project.id", "Annotation.projectId") - .innerJoin("User", "User.id", "Annotation.userId") - .where("Annotation.projectId", projectId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .orderBy("Annotation.startTime", "asc"); -} - -export function selectOne( - annotation: string | { id: string }, - user?: Partial -) { - let annotationId = annotation; - - if (typeof annotation === "object") { - annotationId = annotation.id; - } - - return database - .select( - database.raw('"Annotation".*'), - database.raw( - "json_build_object(" + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"' - ) - ) - .from("Annotation") - .innerJoin("Project", "Project.id", "Annotation.projectId") - .innerJoin("User", "User.id", "Annotation.userId") - .where("Annotation.id", annotationId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .first() - .then((row?: AnnotationRecord) => - row - ? Promise.resolve(row) - : Promise.reject(new Error("AnnotationNotFound")) - ); -} - -export function insert( - annotation: AnnotationData, - user: UserRecord, - projectId: string -) { - return database("Annotation") - .insert({ - text: annotation.text, - startTime: annotation.startTime, - stopTime: annotation.stopTime, - pause: annotation.pause, - userId: user.id, - projectId: projectId, - }) - .returning("id") - .then(getExactlyOne) - .then((id) => selectOne(id, user)); -} - -export function update(id: string, data: AnnotationData, user: UserRecord) { - return database("Annotation") - .update({ - text: data.text, - startTime: data.startTime, - stopTime: data.stopTime, - pause: data.pause, - }) - .returning("id") - .where("id", id) - .then(getExactlyOne) - .then(() => selectOne(id, user)); -} - -export function del(id: string) { - return database("Annotation").where("id", id).del(); -} diff --git a/apps/server/src/store/CommentStore.ts b/apps/server/src/store/CommentStore.ts deleted file mode 100644 index a57b0901..00000000 --- a/apps/server/src/store/CommentStore.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { CommentRecord, UserRecord } from '@celluloid/types'; -import { Knex } from 'knex'; - -import { database, getExactlyOne } from '../backends/Database'; -import { Logger } from '../backends/Logger'; -import * as ProjectStore from './ProjectStore'; - -export function selectByAnnotation(annotationId: string, user: Partial) { - return database.select( - database.raw('"Comment".*'), - database.raw( - 'json_build_object(' + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"')) - .from('Comment') - .innerJoin('Annotation', 'Annotation.id', 'Comment.annotationId') - .innerJoin('User', 'User.id', 'Comment.userId') - .innerJoin('Project', 'Project.id', 'Annotation.projectId') - .where('Comment.annotationId', annotationId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.where('User.id', user.id); - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .orderBy('Comment.createdAt', 'asc'); -} - -export function selectOne(commentId: string) { - - console.log(commentId, "selectOne") - return database.select( - database.raw('"Comment".*'), - database.raw( - 'json_build_object(' + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"')) - .from('Comment') - .innerJoin('User', 'User.id', 'Comment.userId') - .where('Comment.id', commentId) - .first() - .then((row?: CommentRecord) => row ? Promise.resolve(row) : - Promise.reject(new Error('CommentNotFound'))); -} - -export function insert(annotationId: string, text: string, user: Partial) { - return database('Comment') - .insert({ - annotationId, - userId: user.id, - text, - createdAt: database.raw('NOW()') - }) - .returning('id') - .then(getExactlyOne) - .then(row => selectOne(row.id)); -} - -export function update(id: string, text: string) { - return database('Comment') - .update({ - text - }) - .where('id', id) - .returning('id') - .then(getExactlyOne) - .then(() => selectOne(id)); -} - -export function del(id: string) { - return database('Comment') - .where('id', id) - .del(); -} \ No newline at end of file diff --git a/apps/server/src/store/ProjectStore.ts b/apps/server/src/store/ProjectStore.ts deleted file mode 100644 index b01082d4..00000000 --- a/apps/server/src/store/ProjectStore.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - ProjectCreateData, - ProjectRecord, - ProjectShareData, - UserRecord, -} from "@celluloid/types"; -import { Knex } from "knex"; - -import { generateUniqueShareName } from "../auth/Utils"; -import { - database, - filterNull, - getExactlyOne, - hasConflictedOn, -} from "../backends/Database"; -import { logger } from "../backends/Logger"; -import { Project, User } from "../knex"; -import { tagProject } from "./TagStore"; - -const log = logger("store/ProjectStore"); - -export const orIsMember = (nested: Knex.QueryBuilder, user?: UserRecord) => - user - ? nested.orWhereIn( - "Project.id", - database - .select("projectId") - .from("UserToProject") - .where("userId", user.id) - ) - : nested; - -export const orIsOwner = (nested: Knex.QueryBuilder, user?: UserRecord) => - user ? nested.orWhere("Project.userId", user.id) : nested; - -function filterUserProps({ id, username, role }: UserRecord) { - return { - id, - username, - role, - }; -} - -export function isOwnerOrCollaborativeMember( - projectId: string, - user: UserRecord -) { - return Promise.all([ - isOwner(projectId, user), - isCollaborativeMember(projectId, user), - ]).then(([owner, member]: boolean[]) => owner || member); -} - -export function isOwner(projectId: string, user: UserRecord) { - return database - .first("id") - .from("Project") - .where("id", projectId) - .andWhere("userId", user.id) - .then((row: string) => (row ? true : false)); -} - -export function isMember(projectId: string, user: Partial) { - return ( - database - .first("projectId") - .from("UserToProject") - .where("UserToProject.projectId", projectId) - // @ts-ignore - .andWhere("UserToProject.userId", user.id) - .then((row: string) => (row ? true : false)) - ); -} - -export function isCollaborativeMember(projectId: string, user: UserRecord) { - return database - .first("projectId") - .from("UserToProject") - .innerJoin("Project", "Project.id", "UserToProject.projectId") - .where("UserToProject.projectId", projectId) - .andWhere("UserToProject.userId", user.id) - .andWhere("Project.collaborative", true) - .then((row: string) => (row ? true : false)); -} - -// < Project[] &{ -// tags: Tag[], -// user: User -// }> -export function selectAll(user: UserRecord): Promise { - return database("projects") - .select( - database.raw('"Project".*'), - // database.raw(`to_json(array_agg("Tag")) AS "tags"`), - database.raw(`row_to_json("User") as "user"`) - ) - .from("Project") - .innerJoin("User", "User.id", "Project.userId") - // .leftJoin("TagToProject", "Project.id", "TagToProject.projectId") - // .leftJoin("Tag", "Tag.id", "TagToProject.tagId") - .where("Project.public", true) - .modify(orIsOwner, user) - .modify(orIsMember, user) - .groupBy("Project.id", "User.id") - .then((rows) => - rows.map((r: any) => ({ - ...r, - user: filterUserProps(r.user), - }) - ) - ); -} - - - -export function selectOneByShareName(shareCode: string) { - return database.first("*").from("Project").where("shareCode", shareCode); -} - -export function selectOne(projectId: string, user: Partial) { - return database - .first( - database.raw('"Project".*'), - // database.raw(`to_json(array_agg("Tag")) as "tags"`), - database.raw(`row_to_json("User") as "user"`) - ) - .from("Project") - .innerJoin("User", "User.id", "Project.userId") - // .leftJoin("TagToProject", "Project.id", "TagToProject.projectId") - // .leftJoin("Tag", "Tag.id", "TagToProject.tagId") - .where((nested: Knex.QueryBuilder) => { - nested.where("Project.public", true); - nested.modify(orIsMember, user); - nested.modify(orIsOwner, user); - }) - .andWhere("Project.id", projectId) - .groupBy("Project.id", "User.id") - .then((row?) => { - return new Promise((resolve, reject) => { - if (row) { - return selectProjectMembers(projectId).then((members) => - resolve( - { - user: filterUserProps(row.user), - members, - ...row, - } - ) - ); - } else { - return reject(new Error("ProjectNotFound")); - } - }); - }); -} - -export function insert(project: ProjectCreateData, user: UserRecord) { - const INSERT_RETRY_COUNT = 20; - const { tags, ...props } = project; - const query: any = (retry: number) => - database("Project") - .insert({ - ...props, - userId: user.id, - publishedAt: database.raw("NOW()"), - shareName: generateUniqueShareName(props.title, retry), - }) - .returning("*") - .then(getExactlyOne) - .catch((error) => { - if (hasConflictedOn(error, "User", "username")) { - if (retry < INSERT_RETRY_COUNT) { - return query(retry + 1); - } else { - log.warn( - "Failed to insert project: unique share name generation failed" - ); - } - } - throw error; - }); - return query(0).then((record: any) => - Promise.all(project.tags.map((tag) => tagProject(tag.id, record.id))).then( - () => Promise.resolve({ tags, ...record }) - ) - ); -} - -export function update(projectId: string, props: ProjectRecord) { - return database("Project") - .update(props) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function del(projectId: string) { - return database("Project").where("id", projectId).del(); -} - -export function shareById(projectId: string, data: ProjectShareData) { - return database("Project") - .update({ - shared: true, - sharePassword: data.sharePassword, - }) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function unshareById(projectId: string) { - return database("Project") - .update({ - shared: false, - sharePassword: null, - }) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function selectProjectMembers(projectId: string) { - return database - .select("User.id", "User.username", "User.role") - .from("UserToProject") - .innerJoin("User", "User.id", "UserToProject.userId") - .where("UserToProject.projectId", projectId) - .then((rows) => rows.map(filterUserProps)); -} - -export function setPublicById(projectId: string, _public: boolean) { - return database("Project") - .update({ - public: _public, - }) - .where("id", projectId) - .returning("*") - .then(getExactlyOne); -} - -export function setCollaborativeById( - projectId: string, - collaborative: boolean -) { - return database("Project") - .update({ - collaborative, - }) - .where("id", projectId) - .returning("*") - .then(getExactlyOne); -} - diff --git a/apps/server/src/store/TagStore.ts b/apps/server/src/store/TagStore.ts deleted file mode 100644 index 5cca7bba..00000000 --- a/apps/server/src/store/TagStore.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { database, getExactlyOne } from '../backends/Database'; - -export function selectAll() { - return database.select() - .from('Tag'); -} - -export function insert(name: string) { - return database('Tag') - .insert({ - 'name': name, - 'featured': false - }) - .returning('*') - .then(getExactlyOne); -} - -export function tagProject(tagId: string, projectId: string) { - return database('TagToProject') - .insert({ - tagId, - projectId - }); -} - -export function untagProject(tagId: string, projectId: string) { - return database('TagToProject') - .insert({ - tagId, - projectId - }); -} \ No newline at end of file diff --git a/apps/server/src/store/UserStore.ts b/apps/server/src/store/UserStore.ts deleted file mode 100644 index ed2c0d11..00000000 --- a/apps/server/src/store/UserStore.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { UserRecord } from "@celluloid/types"; -import { Knex } from "knex"; - -import { generateConfirmationCode, hashPassword } from "../auth/Utils"; -import { database, getExactlyOne } from "../backends/Database"; - -export function createStudent( - username: string, - password: string, - projectId: string -) { - - - return database.transaction((transaction) => - database("User") - .transacting(transaction) - .insert({ - password: hashPassword(password), - username, - confirmed: false, - role: "Student", - }) - .returning("*") - .then(getExactlyOne) - .then((student) => - joinProject(student.id, projectId, transaction).then(() => - Promise.resolve(student) - ) - ) - .then(transaction.commit) - .catch(transaction.rollback) - ); -} - -export function createTeacher( - username: string, - email: string, - password: string -) { - return database("User") - .insert({ - email, - password: hashPassword(password), - username, - code: generateConfirmationCode(), - codeGeneratedAt: database.raw("NOW()"), - confirmed: false, - role: "Teacher", - }) - .returning("*") - .then(getExactlyOne); -} - -export function updatePasswordByEmail(login: string, password: string) { - return database("User") - .update({ - password: hashPassword(password), - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function updateCodeByEmail(login: string) { - return database("User") - .update({ - code: generateConfirmationCode(), - codeGeneratedAt: database.raw("NOW()"), - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function confirmByEmail(login: string) { - return database("User") - .update({ - code: null, - codeGeneratedAt: null, - confirmed: true, - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function selectOne(id: string) { - return database("User").first().where("id", id); -} - -export function selectOneByUsernameOrEmail(login: string) { - return database("User") - .first() - .where("username", login) - .orWhere("email", login); -} - -function withTransaction( - query: Knex.QueryBuilder, - transaction?: Knex.Transaction -) { - return transaction ? query.transacting(transaction) : query; -} - -export function joinProject( - userId: string, - projectId: string, - transaction?: Knex.Transaction -) { - return withTransaction(database("UserToProject"), transaction).insert({ - userId, - projectId, - }); -} - -export function leaveProject(userId: string, projectId: string) { - return database("UserToProject") - .where("userId", userId) - .andWhere("projectId", projectId) - .del(); -} diff --git a/apps/server/src/types/UserTypes.ts b/apps/server/src/types/UserTypes.ts deleted file mode 100644 index 015754ec..00000000 --- a/apps/server/src/types/UserTypes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - UserRecord, -} from '@celluloid/types'; - -export interface UserServerRecord extends UserRecord { - confirmed: boolean; - password: string; -} - -export interface TeacherServerRecord extends UserServerRecord { - code?: string; - codeExpiresAt?: Date; - email: string; -} - -export interface AdminServerRecord extends UserServerRecord { - code: string; - codeExpiresAt: Date; - email: string; -} diff --git a/apps/server/src/utils/generate-types.ts b/apps/server/src/utils/generate-types.ts deleted file mode 100644 index ff1b8942..00000000 --- a/apps/server/src/utils/generate-types.ts +++ /dev/null @@ -1,9 +0,0 @@ -const { knex } = require("knex"); -const { updateTypes } = require("knex-types"); - -const db = knex(require("../knexfile").development); - -updateTypes(db, { output: "../types.ts" }).catch((err:any) => { - console.error(err); - process.exit(1); -}); \ No newline at end of file diff --git a/apps/server/src/utils/srt.ts b/apps/server/src/utils/srt.ts deleted file mode 100644 index 4e2f1b35..00000000 --- a/apps/server/src/utils/srt.ts +++ /dev/null @@ -1,35 +0,0 @@ -interface Subtitle { - startTime: number; - endTime: number; - text: string; -} - -export function convertToSrt(json: Subtitle[]): string { - let srt = ''; - - json.forEach((subtitle: Subtitle, index: number) => { - const { startTime, endTime, text } = subtitle; - - // Format the start and end time in HH:MM:SS,mmm format - const formattedStartTime = formatTime(startTime); - const formattedEndTime = formatTime(endTime); - - // Add the subtitle index, start and end time, and text to the SRT format - srt += `${index + 1}\n${formattedStartTime} --> ${formattedEndTime}\n${text}\n\n`; - }); - - return srt; -} - -function formatTime(time: number): string { - const hours = Math.floor(time / 3600); - const minutes = Math.floor((time % 3600) / 60); - const seconds = Math.floor(time % 60); - const milliseconds = Math.round((time % 1) * 1000); - - return `${padNumber(hours)}:${padNumber(minutes)}:${padNumber(seconds)},${padNumber(milliseconds, 3)}`; -} - -function padNumber(number: number, length = 2): string { - return number.toString().padStart(length, '0'); -} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json deleted file mode 100644 index 5e89adc9..00000000 --- a/apps/server/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "@celluloid/config/tsconfig/node16.json", - "compilerOptions": { - "paths": { - /* IMPORTANT: this must be the same of 'src/aliases.ts' */ - "~/*": [ - "./*" - ] - }, - }, - "include": [ - "**/*.ts", - "**/*.tsx", - "tsup.config.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/apps/server/tsup.config.ts b/apps/server/tsup.config.ts deleted file mode 100644 index 11a619b2..00000000 --- a/apps/server/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig({ - clean: true, - dts: false, - entry: ["src/index.ts"], - format: ["cjs"], - minify: isProduction, - sourcemap: true, -}); diff --git a/package.json b/package.json index 4d2bcad9..ff5e8256 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "prettier:all": "prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,md}\"", "--shortcuts to run commands in workspaces--": "", "frontend": "yarn workspace frontend", - "server": "yarn workspace server", "admin": "yarn workspace admin", "backend": "yarn workspace backend", "prisma": "yarn workspace @celluloid/prisma", diff --git a/packages/passport/src/index.ts b/packages/passport/src/index.ts index 45101d08..928d38d2 100644 --- a/packages/passport/src/index.ts +++ b/packages/passport/src/index.ts @@ -2,5 +2,6 @@ export * from "./errors"; export { createSession } from "./session"; import passport from './passport'; +export * from "./passport"; export { passport }; diff --git a/packages/passport/src/passport.ts b/packages/passport/src/passport.ts index 97ff6253..32209d47 100644 --- a/packages/passport/src/passport.ts +++ b/packages/passport/src/passport.ts @@ -46,27 +46,25 @@ passport.use( ); -const loginStrategy = new LocalStrategy( - { usernameField: "login" }, - async (login, password, done) => { +const loginStrategy = new LocalStrategy(async (login, password, done) => { - const user = await prisma.user.findFirst({ - where: { - OR: [{ email: login }, { username: login, }] - } - }); - - if (!user) { - return Promise.resolve(done(new InvalidUserError("User not found"))); - } - if (!bcrypt.compareSync(password, user.password)) { - return Promise.resolve(done(new InvalidUserError(`Login failed for user ${user.username}: incorrect password`))); + const user = await prisma.user.findFirst({ + where: { + OR: [{ email: login }, { username: login, }] } - if (!user.confirmed && user.role !== UserRole.Student) { - return Promise.resolve(done(new UserNotConfirmed(`Login failed: ${user.username} is not confirmed`))); - } - return Promise.resolve(done(null, user)); + }); + + if (!user) { + return done(new InvalidUserError("User not found")); + } + if (!bcrypt.compareSync(password, user.password)) { + return done(new InvalidUserError(`Login failed for user ${user.username}: incorrect password`)); } + if (!user.confirmed && user.role !== UserRole.Student) { + return done(new UserNotConfirmed(`Login failed: ${user.username} is not confirmed`)); + } + return done(null, user); +} ); passport.use(SigninStrategy.LOGIN, loginStrategy); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index c79991fb..447fe1c4 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -24,6 +24,7 @@ "dev": "tsup --watch" }, "dependencies": { + "@celluloid/passport": "*", "@celluloid/prisma": "*", "@trpc/server": "^10.40.0", "bcryptjs": "^2.4.3", @@ -33,6 +34,9 @@ "express-session": "^1.17.3", "js2xmlparser": "^5.0.0", "lodash": "^4.17.21", + "mjml": "^4.14.1", + "nodemailer": "^6.9.7", + "nodemailer-smtp-transport": "^2.7.4", "papaparse": "^5.4.1", "trpc-openapi": "^1.2.0", "uuid": "^9.0.1", @@ -41,6 +45,7 @@ "devDependencies": { "@celluloid/config": "*", "@types/express-session": "^1.17.8", + "@types/mjml": "^4.7.3", "@types/uuid": "^9.0.4", "tsup": "^7.2.0" } diff --git a/packages/trpc/src/mailer/sendMail.ts b/packages/trpc/src/mailer/sendMail.ts new file mode 100644 index 00000000..2dea4e91 --- /dev/null +++ b/packages/trpc/src/mailer/sendMail.ts @@ -0,0 +1,93 @@ +import mjml2html from 'mjml'; +import * as nodemailer from "nodemailer"; + +import getTransport from "./transport"; + +const EMAIL_FROM = process.env.EMAIL_FROM || "no-reply@celluloid.huma-num.fr"; + +const isDev = process.env.NODE_ENV !== "production"; + + +export async function sendMail( + to: string, subject: string, html: string) { + const transport = await getTransport(); + const mailOptions = { + from: `Celluloid <${EMAIL_FROM}>`, to, subject, html + }; + + const info = await transport.sendMail(mailOptions); + + if (isDev) { + const url = nodemailer.getTestMessageUrl(info); + if (url) { + // Hex codes here equivalent to chalk.blue.underline + console.log( + `Development email preview: \x1B[34m\x1B[4m${url}\x1B[24m\x1B[39m` + ); + } + } + +} + + +export async function sendPasswordReset({ username, email, code }: { username: string, email: string, code: string }) { + const subject = `${username} : réinitialisation de votre mot de passe Celluloid`; + + const mjmlTemplate = ` + + + + + Bonjour ${username}, + Nous avons reçu une demande de réinitialisation de mot de passe pour l'adresse email ${email} + Voici votre code de confirmation : ${code} + Ce code sera valable pendant 1 heure. + Veuillez le saisir dans le formulaire prévu à cet effet. + Si vous n'êtes pas à l'origine de cette demande, veuillez simplement ignorer ce mail. + Cordialement, + L'équipe Celluloid + + + + + `; + + const { html } = mjml2html(mjmlTemplate); + await sendMail(email, subject, html) +} + + + +export async function sendConfirmationCode({ username, email, code }: { username: string, email: string, code: string }) { + const subject = `${username} : réinitialisation de votre mot de passe Celluloid`; + + const mjmlTemplate = ` + + + + + + Bonjour ${username}, + + + Voici votre code de confirmation : ${code} + + + Ce code est valable pendant 1 heure. + + + Veuillez le saisir dans le formulaire prévu à cet effet. + + + L'équipe Celluloid vous souhaite la bienvenue ! + + + + + + `; + + const { html } = mjml2html(mjmlTemplate); + await sendMail(email, subject, html) +} + diff --git a/packages/trpc/src/mailer/transport.ts b/packages/trpc/src/mailer/transport.ts new file mode 100644 index 00000000..45a56c12 --- /dev/null +++ b/packages/trpc/src/mailer/transport.ts @@ -0,0 +1,72 @@ +import { promises as fsp } from "fs"; +import * as nodemailer from "nodemailer"; + +const { readFile, writeFile } = fsp; + +const isTest = process.env.NODE_ENV === "test"; +const isDev = process.env.NODE_ENV !== "production"; + +let transporterPromise: Promise; +const etherealFilename = `${process.cwd()}/.ethereal`; + +let logged = false; + +export default function getTransport(): Promise { + if (!transporterPromise) { + transporterPromise = (async () => { + if (isTest) { + return nodemailer.createTransport({ + jsonTransport: true, + }); + } else if (isDev) { + let account; + try { + const testAccountJson = await readFile(etherealFilename, "utf8"); + account = JSON.parse(testAccountJson); + } catch (e: any) { + account = await nodemailer.createTestAccount(); + await writeFile(etherealFilename, JSON.stringify(account)); + } + if (!logged) { + logged = true; + console.log(); + console.log(); + console.log( + // Escapes equivalent to chalk.bold + "\x1B[1m" + + " ✉️ Emails in development are sent via ethereal.email; your credentials follow:" + + "\x1B[22m" + ); + console.log(" Site: https://ethereal.email/login"); + console.log(` Username: ${account.user}`); + console.log(` Password: ${account.pass}`); + console.log(); + console.log(); + } + return nodemailer.createTransport({ + host: "smtp.ethereal.email", + port: 587, + secure: false, + auth: { + user: account.user, + pass: account.pass, + }, + }); + } else { + if (!process.env.SMTP_HOST) { + throw new Error("Misconfiguration: no SMTP_HOST"); + } + if (!process.env.SMTP_PORT) { + throw new Error("Misconfiguration: no SMTP_PORT"); + } + return nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || "465", 10), + secure: process.env.SMTP_SECURE === 'true', + }); + } + })(); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return transporterPromise!; +} diff --git a/packages/trpc/src/routers/user.ts b/packages/trpc/src/routers/user.ts index db57cee6..08db383f 100644 --- a/packages/trpc/src/routers/user.ts +++ b/packages/trpc/src/routers/user.ts @@ -1,30 +1,381 @@ -import { prisma } from "@celluloid/prisma" +import { passport, SigninStrategy } from "@celluloid/passport"; +import { Prisma, prisma, UserRole } from "@celluloid/prisma" import { TRPCError } from "@trpc/server"; import { z } from 'zod'; +import { sendConfirmationCode, sendPasswordReset } from "../mailer/sendMail"; import { protectedProcedure, publicProcedure, router } from '../trpc'; +import { compareCodes, generateOtp, hashPassword } from "../utils/forgot"; + +export const defaultUserSelect = Prisma.validator()({ + id: true, + username: true, + role: true, + initial: true, + color: true, +}); export const UserSchema = z.object({ - id: z.string(), - username: z.string(), - role: z.string(), - initial: z.string(), - color: z.string(), + id: z.string({ description: 'The unique identifier for the user' }), + username: z.string({ description: 'The username for the user' }), + role: z.nativeEnum(UserRole, { description: 'The role assigned to the user, either Admin or User' }).nullable(), + initial: z.string({ description: 'The initial letter or string for user representation' }), + color: z.string({ description: 'The color code associated with the user' }) }); export const userRouter = router({ - login: publicProcedure.input( + login: publicProcedure + .meta({ + openapi: { + method: 'POST', + path: '/login', + description: 'This endpoint allows a user to login.' + } + }) + .input( + z.object({ + username: z.string({ description: 'The username of the user' }), + password: z.string({ description: 'The password for the user' }) + }), + ).output(UserSchema.nullable()) + .mutation(async ({ ctx, input }) => { + + ctx.req.body = input; + + await new Promise((resolve, reject) => { + passport.authenticate(SigninStrategy.LOGIN, { + failWithError: true + })(ctx.req, ctx.res, (err: Error, user: Express.User) => { + if (err) return reject(err); + resolve(user); + }) + }).catch(err => { + console.log(err.name); + + if (err?.name === 'AuthenticationError') { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Incorrect username or password.' + }) + } else if (err?.name === "UserNotConfirmed") { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'UserNotConfirmed' + }) + } + + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: err + }) + }) + + const user = await prisma.user.findFirst({ + select: defaultUserSelect, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } + }); + + return user; + }), + forgot: publicProcedure.input( + z.object({ + email: z.string() + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true }, + where: { + OR: [{ email: input.email }, { username: input.email, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + if (!user.email) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `User account doesn't have email address`, + }); + } + + const otp = generateOtp(); + await prisma.user.update({ + where: { id: user.id }, + data: { + code: otp, + codeGeneratedAt: new Date() + } + }) + await sendPasswordReset({ email: user.email, username: user.username, code: otp }) + return { status: true } + }), + + recover: publicProcedure.input( + z.object({ + username: z.string(), + code: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + + if (!compareCodes(input.code, user.code || "")) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Failed to recover account, code invalid` + }); + } + + const newUser = await prisma.user.update({ + where: { id: user.id }, + data: { + password: hashPassword(input.password), + code: null, + codeGeneratedAt: null, + confirmed: true + } + }) + + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + + return { status: true } + + }), + + register: publicProcedure.input( + z.object({ + username: z.string(), + email: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + + + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.email }, { username: input.username, }] + } + }); + + if (user) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `ACCOUNT_EXISTS`, + }); + } + + const code = generateOtp(); + const newUser = await prisma.user.create({ + select: { + ...defaultUserSelect, email: true + }, + data: { + username: input.username, + email: input.email, + password: hashPassword(input.password), + code: code, + codeGeneratedAt: new Date(), + confirmed: false, + role: "Teacher" + } + }) + + await sendConfirmationCode({ username: newUser.username, email: input.email, code: code }); + + return newUser + + }), + registerAsStudent: publicProcedure.input( + z.object({ + username: z.string(), + shareCode: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + + + const project = await prisma.project.findUnique({ + where: { shareCode: input.shareCode } + + }); + + if (!project) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `CODE_NOT_FOUND`, + }); + } + + + const user = await prisma.user.findFirst({ + select: defaultUserSelect, + where: { + username: input.username + } + }); + + if (user) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `ACCOUNT_EXISTS`, + }); + } + + + const newUser = await prisma.user.create({ + select: { + ...defaultUserSelect + }, + data: { + username: input.username, + password: hashPassword(input.password), + confirmed: true, + role: "Student" + } + }) + + await prisma.project.update({ + where: { id: project.id }, + data: { + members: { + create: [{ + userId: newUser.id, + }], + } + } + }) + + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + + return { projectId: project.id } + + }), + join: protectedProcedure.input( + z.object({ + shareCode: z.string(), + }), + ).mutation(async ({ ctx, input }) => { + + const project = await prisma.project.findUnique({ + where: { shareCode: input.shareCode } + + }); + + if (!project) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `CODE_NOT_FOUND`, + }); + } + + await prisma.project.update({ + where: { id: project.id }, + data: { + members: { + create: [{ + userId: ctx.user?.id + }], + } + } + }) + return { projectId: project.id } + }), + + askEmailConfirm: publicProcedure.input( + z.object({ + email: z.string() + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true }, + where: { + OR: [{ email: input.email }, { username: input.email, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + if (!user.email) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `User account doesn't have email address`, + }); + } + + const otp = generateOtp(); + await prisma.user.update({ + where: { id: user.id }, + data: { + code: otp, + codeGeneratedAt: new Date() + } + }) + await sendConfirmationCode({ email: user.email, username: user.username, code: otp }) + return { status: true } + }), + + confirm: publicProcedure.input( z.object({ username: z.string(), - password: z.string() + code: z.string(), }), - ).mutation(async ({ input }) => { - ; - throw new TRPCError({ - code: 'UNAUTHORIZED', + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + + if (!compareCodes(input.code, user.code || "")) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Failed to confirm account, code invalid` + }); + } + + const newUser = await prisma.user.update({ + where: { id: user.id }, + data: { + code: null, + codeGeneratedAt: null, + confirmed: true + } + }) + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + return { status: true } }), list: protectedProcedure.query(async () => { @@ -41,6 +392,7 @@ export const userRouter = router({ if (ctx.user) { // Retrieve the user with the given ID const user = await prisma.user.findUnique({ + select: { ...defaultUserSelect, email: true }, where: { id: ctx.user.id } }); return user; @@ -54,7 +406,7 @@ export const userRouter = router({ ).query(async (opts) => { const { input } = opts; // Retrieve the user with the given ID - const user = await prisma.user.findUnique({ where: { id: input.id } }); + const user = await prisma.user.findUnique({ select: defaultUserSelect, where: { id: input.id } }); return user; }), projects: protectedProcedure @@ -86,13 +438,7 @@ export const userRouter = router({ }, include: { user: { - select: { - id: true, - username: true, - role: true, - initial: true, - color: true - } + select: defaultUserSelect }, members: true, playlist: { diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index a631a35c..b15067a2 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -3,7 +3,7 @@ import "express-session" import { User, UserRole } from '@celluloid/prisma'; import { initTRPC, TRPCError } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; -import { Request } from "express"; +import { type Request, type Response } from 'express'; import { Session } from "express-session"; import { OpenApiMeta } from 'trpc-openapi'; import { v4 as uuid } from 'uuid'; @@ -13,6 +13,8 @@ export type Context = { requestId: string; requirePermissions: (roles: UserRole[]) => boolean; logout: () => Promise; + req: Request; + res: Response; }; export const createRPCContext = async ({ @@ -47,9 +49,13 @@ export const createRPCContext = async ({ }); }) } - return { user, requirePermissions, logout, requestId }; + return { + user, requirePermissions, logout, requestId, req, + res, + }; }; + const t = initTRPC.context().meta().create({ // transformer: SuperJSON }); diff --git a/packages/trpc/src/utils/forgot.ts b/packages/trpc/src/utils/forgot.ts new file mode 100644 index 00000000..a7d0c44f --- /dev/null +++ b/packages/trpc/src/utils/forgot.ts @@ -0,0 +1,19 @@ + +import bcrypt from 'bcryptjs'; + +export function hashPassword(password: string) { + const salt = bcrypt.genSaltSync(); + return bcrypt.hashSync(password, salt); +} + + +export function generateOtp() { + // Generate a 4-digit OTP + const otp: string = Math.floor(1000 + Math.random() * 9000).toString(); + return otp; +} + + +export function compareCodes(expected: string, actual: string) { + return expected.replace(/\s/g, "") === actual.replace(/\s/g, ""); +} diff --git a/packages/types/src/AnnotationTypes.ts b/packages/types/src/AnnotationTypes.ts deleted file mode 100644 index d61230fa..00000000 --- a/packages/types/src/AnnotationTypes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CommentRecord } from './CommentTypes'; -import { UserRecord } from './UserTypes'; - -export interface AnnotationData { - text: string; - startTime: number; - stopTime: number; - pause: boolean; -} - -export interface AnnotationRecord extends AnnotationData { - projectId: string; - userId: string; - id: string; - user: UserRecord; - comments: CommentRecord[]; -} \ No newline at end of file diff --git a/packages/types/src/CommentTypes.ts b/packages/types/src/CommentTypes.ts deleted file mode 100644 index c53bccb0..00000000 --- a/packages/types/src/CommentTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UserRecord } from './UserTypes'; - -export interface CommentRecord { - id: string; - annotationId: string; - userId: string; - text: string; - createdAt: Date; - user: UserRecord; -} \ No newline at end of file diff --git a/packages/types/src/ProjectTypes.ts b/packages/types/src/ProjectTypes.ts deleted file mode 100644 index e25b2c0a..00000000 --- a/packages/types/src/ProjectTypes.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { TagData } from './TagTypes'; -import { UserRecord } from './UserTypes'; - -export interface ProjectCreateData { - videoId: string; - title: string; - host: string; - description?: string; - objective: string; - assignments: Array; - tags: TagData[]; - levelStart: number; - levelEnd: number; - public: boolean; - collaborative: boolean; -} - -export interface ProjectUpdateData { - title: string; - description?: string; - objective: string; - assigments: Array; - tags: TagData[]; - levelStart: number; - leverEnd: number; - public: boolean; - collaborative: boolean; -} - -export interface ProjectRecord extends ProjectCreateData { - id: string; - userId: string; - publishedAt: Date; - shared: boolean; - shareName: string; - sharePassword: string; - shareExpiresAt: string; -} - -export interface ProjectGraphRecord extends ProjectRecord { - user: UserRecord; - members: UserRecord[]; -} - -export interface ProjectShareData { - sharePassword: string; - // shareExpiresAt: Date; - // shareMaxUsers: number; -} diff --git a/packages/types/src/TagTypes.ts b/packages/types/src/TagTypes.ts deleted file mode 100644 index ce2e89b5..00000000 --- a/packages/types/src/TagTypes.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface TagData { - id: string; - name: string; - featured: boolean; -} \ No newline at end of file diff --git a/packages/types/src/UnfurlTypes.ts b/packages/types/src/UnfurlTypes.ts deleted file mode 100644 index 977bf3a5..00000000 --- a/packages/types/src/UnfurlTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface UnfurlData { - website: string; - faviconUrl?: string; - title: string; - description?: string; - imageUrl?: string; -} \ No newline at end of file diff --git a/packages/types/src/UserTypes.ts b/packages/types/src/UserTypes.ts deleted file mode 100644 index 6bec594d..00000000 --- a/packages/types/src/UserTypes.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { TagData } from './TagTypes'; - -export interface SigninErrors { - login?: string; - password?: string; - email?: string; - username?: string; - confirmPassword?: string; - code?: string; - server?: string; - shareCode?: string; -} - -export interface SigninResult { - success: boolean; - errors: SigninErrors; -} - -export interface TeacherData { - email: string; - username: string; - subjects?: TagData[]; -} - -export interface TeacherRecord extends TeacherData { - id: string; -} - -export interface TeacherSignupData extends TeacherData { - password: string; -} - -export interface TeacherConfirmData { - login: string; - code: string; -} - -export interface Credentials { - login: string; - password: string; -} - -export interface StudentData { - username: string; -} - -export interface StudentRecord extends StudentData { - id: string; -} - -export interface StudentSignupData { - username: string; - password: string; - shareCode: string; -} - -export interface TeacherConfirmResetPasswordData extends TeacherConfirmData { - password: string; -} - -export interface ConfirmSignupErrors { - email: string; - code: string; -} - -export interface ConfirmSignupValidation { - success: boolean; - errors?: ConfirmSignupErrors; -} - -type UserRole - = 'Admin' - | 'Teacher' - | 'Student' - ; - -export interface UserRecord { - id: string; - username: string; - role: UserRole; - email?: string; -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fdf4de9b..bee8dd37 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,8 +1,2 @@ -export * from "./AnnotationTypes"; -export * from "./CommentTypes"; export * from "./PeerTubeVideo"; export * from "./PeerTubeVideoMetadata"; -export * from "./ProjectTypes"; -export * from "./TagTypes"; -export * from "./UnfurlTypes"; -export * from "./UserTypes"; diff --git a/packages/validators/package.json b/packages/validators/package.json deleted file mode 100644 index 313c15c3..00000000 --- a/packages/validators/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@celluloid/validators", - "main": "dist/index.js", - "version": "0.1.0", - "description": "Validation library for Celluloid client and server types", - "repository": "http://github.com/celluloid-edu/celluloid", - "author": "Younes Benaomar ", - "license": "MIT", - "scripts": { - "build": "tsup", - "dev": "tsup --watch --silent" - }, - "devDependencies": { - "@celluloid/types": "*", - "@types/validator": "13.7.10", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "validator": "13.7.0" - } -} diff --git a/packages/validators/src/UserValidator.ts b/packages/validators/src/UserValidator.ts deleted file mode 100644 index df4de555..00000000 --- a/packages/validators/src/UserValidator.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - Credentials, - SigninResult, - StudentSignupData, - TeacherConfirmData, - TeacherConfirmResetPasswordData, - TeacherSignupData, -} from '@celluloid/types'; -import validator from 'validator'; - -export function validateSignup(payload: TeacherSignupData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.username !== 'string' || - payload.username.trim().length === 0) { - result.success = false; - result.errors.username = `UsernameMissing`; - } - - if (!payload || typeof payload.email !== 'string' || - !validator.isEmail(payload.email)) { - result.success = false; - result.errors.email = 'InvalidEmailFormat'; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length < 8) { - result.success = false; - result.errors.password = 'InvalidPasswordFormat'; - } - return result; -} - -export function validateConfirmationCode(code: string): boolean { - const codeRegExp = /^[0-9]{6}$/; - const trimmedCode = code.replace(/\s/g, ''); - return codeRegExp.test(trimmedCode); -} - -export function validateConfirmResetPassword( - payload: TeacherConfirmResetPasswordData -) { - const result = { - success: true, - errors: {} - } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.login = 'MissingLogin'; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length < 8) { - result.success = false; - result.errors.password = 'InvalidPasswordFormat'; - } - - if (!payload || typeof payload.code !== 'string' || - !validateConfirmationCode(payload.code)) { - result.success = false; - result.errors.code = `InvalidCodeFormat`; - } - return result; -} - -export function validateConfirmSignup(payload: TeacherConfirmData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.email = 'MissingLogin'; - } - - if (!payload || typeof payload.code !== 'string' || - !validateConfirmationCode(payload.code)) { - result.success = false; - result.errors.code = `InvalidCodeFormat`; - } - - return result; -} - -export function validateLogin(payload: Credentials) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.login = `MissingLogin`; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length === 0) { - result.success = false; - result.errors.password = 'MissingPassword'; - } - - return result; -} - -export function validateStudentSignup(payload: StudentSignupData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.shareCode !== 'string' || - payload.shareCode.trim().length === 0) { - result.success = false; - result.errors.shareCode = `MissingShareCode`; - } - - if (!payload || typeof payload.username !== 'string' || - payload.username.trim().length === 0) { - result.success = false; - result.errors.username = `MissingUsername`; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length === 0) { - result.success = false; - result.errors.password = 'MissingPassword'; - } - - return result; -} diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts deleted file mode 100644 index 80e5c679..00000000 --- a/packages/validators/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './UserValidator'; \ No newline at end of file diff --git a/packages/validators/tsconfig.json b/packages/validators/tsconfig.json deleted file mode 100644 index 847685fc..00000000 --- a/packages/validators/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@celluloid/config/tsconfig/node16.json", - "include": [ - "**/*.ts", - "**/*.tsx", - "tsup.config.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/packages/validators/tsup.config.ts b/packages/validators/tsup.config.ts deleted file mode 100644 index db41265e..00000000 --- a/packages/validators/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig({ - clean: true, - dts: true, - entry: ["src/index.ts"], - format: ["esm", "cjs"], - minify: isProduction, - sourcemap: true, -}); diff --git a/yarn.lock b/yarn.lock index af7b4b9f..b9b80b6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1770,7 +1770,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.23.1": +"@babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.23.1": version: 7.23.2 resolution: "@babel/runtime@npm:7.23.2" dependencies: @@ -1918,9 +1918,11 @@ __metadata: resolution: "@celluloid/trpc@workspace:packages/trpc" dependencies: "@celluloid/config": "*" + "@celluloid/passport": "*" "@celluloid/prisma": "*" "@trpc/server": ^10.40.0 "@types/express-session": ^1.17.8 + "@types/mjml": ^4.7.3 "@types/uuid": ^9.0.4 bcryptjs: ^2.4.3 change-case: ^4.1.2 @@ -1929,6 +1931,9 @@ __metadata: express-session: ^1.17.3 js2xmlparser: ^5.0.0 lodash: ^4.17.21 + mjml: ^4.14.1 + nodemailer: ^6.9.7 + nodemailer-smtp-transport: ^2.7.4 papaparse: ^5.4.1 trpc-openapi: ^1.2.0 tsup: ^7.2.0 @@ -3560,6 +3565,13 @@ __metadata: languageName: node linkType: hard +"@one-ini/wasm@npm:0.1.1": + version: 0.1.1 + resolution: "@one-ini/wasm@npm:0.1.1" + checksum: 11de17108eae57c797e552e36b259398aede999b4a689d78be6459652edc37f3428472410590a9d328011a8751b771063a5648dd5c4205631c55d1d58e313156 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3709,26 +3721,6 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:^1.8.6": - version: 1.9.6 - resolution: "@reduxjs/toolkit@npm:1.9.6" - dependencies: - immer: ^9.0.21 - redux: ^4.2.1 - redux-thunk: ^2.4.2 - reselect: ^4.1.8 - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - checksum: 61d445f7e084c79f9601f61fcfc4eb65152b850b2a4330239d982297605bd870e63dc1e0211deb3822392cd3bc0c88ca0cdb236a9711a4311dfb199c607b6ac5 - languageName: node - linkType: hard - "@remirror/core-constants@npm:^2.0.2": version: 2.0.2 resolution: "@remirror/core-constants@npm:2.0.2" @@ -5507,6 +5499,22 @@ __metadata: languageName: node linkType: hard +"@types/mjml-core@npm:*": + version: 4.7.3 + resolution: "@types/mjml-core@npm:4.7.3" + checksum: c6d002cc599806a9603e48f3b848a48c54a01a9b8cbc147b1cf01355e80a0ed1be720928e9e290c0dc876ba826fb1f84e7620d8e6edf53b48ce27fa06a73e6e2 + languageName: node + linkType: hard + +"@types/mjml@npm:^4.7.3": + version: 4.7.3 + resolution: "@types/mjml@npm:4.7.3" + dependencies: + "@types/mjml-core": "*" + checksum: c7d31acaea2495cd58bb3ce89f0cb4e52e938be6cfb4262f5be4a3887f24eed50674324d5a25a1797e48c1c7ee295dc2e21c73d45f47b02f1d0a6bcfc14009e4 + languageName: node + linkType: hard + "@types/moment-duration-format@npm:^2.2.0": version: 2.2.4 resolution: "@types/moment-duration-format@npm:2.2.4" @@ -6789,6 +6797,13 @@ __metadata: languageName: node linkType: hard +"ansi-colors@npm:^4.1.1": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: a9c2ec842038a1fabc7db9ece7d3177e2fe1c5dc6f0c51ecfbf5f39911427b89c00b5dc6b8bd95f82a26e9b16aaae2e83d45f060e98070ce4d1333038edceb0e + languageName: node + linkType: hard + "ansi-escapes@npm:^3.0.0, ansi-escapes@npm:^3.2.0": version: 3.2.0 resolution: "ansi-escapes@npm:3.2.0" @@ -8020,6 +8035,16 @@ __metadata: languageName: node linkType: hard +"camel-case@npm:^3.0.0": + version: 3.0.0 + resolution: "camel-case@npm:3.0.0" + dependencies: + no-case: ^2.2.0 + upper-case: ^1.1.1 + checksum: 4190ed6ab8acf4f3f6e1a78ad4d0f3f15ce717b6bfa1b5686d58e4bcd29960f6e312dd746b5fa259c6d452f1413caef25aee2e10c9b9a580ac83e516533a961a + languageName: node + linkType: hard + "camel-case@npm:^4.1.2": version: 4.1.2 resolution: "camel-case@npm:4.1.2" @@ -8313,7 +8338,7 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:^1.0.0-rc.2, cheerio@npm:^1.0.0-rc.3": +"cheerio@npm:1.0.0-rc.12, cheerio@npm:^1.0.0-rc.12, cheerio@npm:^1.0.0-rc.2, cheerio@npm:^1.0.0-rc.3": version: 1.0.0-rc.12 resolution: "cheerio@npm:1.0.0-rc.12" dependencies: @@ -8328,7 +8353,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:^3.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -8389,6 +8414,15 @@ __metadata: languageName: node linkType: hard +"clean-css@npm:^4.2.1": + version: 4.2.4 + resolution: "clean-css@npm:4.2.4" + dependencies: + source-map: ~0.6.0 + checksum: 045ff6fcf4b5c76a084b24e1633e0c78a13b24080338fc8544565a9751559aa32ff4ee5886d9e52c18a644a6ff119bd8e37bc58e574377c05382a1fb7dbe39f8 + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.2 resolution: "clean-css@npm:5.3.2" @@ -8768,7 +8802,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.0": +"commander@npm:^6.1.0, commander@npm:^6.2.0": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 @@ -8899,6 +8933,16 @@ __metadata: languageName: node linkType: hard +"config-chain@npm:^1.1.13": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: ^1.3.4 + proto-list: ~1.2.1 + checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab + languageName: node + linkType: hard + "configstore@npm:^5.0.1": version: 5.0.1 resolution: "configstore@npm:5.0.1" @@ -8936,29 +8980,6 @@ __metadata: languageName: node linkType: hard -"connected-react-router@npm:6.9.3": - version: 6.9.3 - resolution: "connected-react-router@npm:6.9.3" - dependencies: - immutable: ^3.8.1 || ^4.0.0 - lodash.isequalwith: ^4.4.0 - prop-types: ^15.7.2 - seamless-immutable: ^7.1.3 - peerDependencies: - history: ^4.7.2 - react: ^16.4.0 || ^17.0.0 - react-redux: ^6.0.0 || ^7.1.0 - react-router: ^4.3.1 || ^5.0.0 - redux: ^3.6.0 || ^4.0.0 - dependenciesMeta: - immutable: - optional: true - seamless-immutable: - optional: true - checksum: 047a11c2f3c9993087f3cd467789445781320c61f3184e1016e7b05862a275006004867231cc7396c7f213afd2fbce7e8ac0df39ba2cda7502d72a140657f9e7 - languageName: node - linkType: hard - "consola@npm:^3.2.3": version: 3.2.3 resolution: "consola@npm:3.2.3" @@ -10046,6 +10067,13 @@ __metadata: languageName: node linkType: hard +"detect-node@npm:2.0.4": + version: 2.0.4 + resolution: "detect-node@npm:2.0.4" + checksum: c06ae40fefbad8cb8cbb6ca819c93568b2a809e747bfc9c71f3524b027f5e988163b0ac0517fd65288b375360b30bc4822172eb05d211f99003d73cf8ec22911 + languageName: node + linkType: hard + "detect-node@npm:^2.0.4": version: 2.1.0 resolution: "detect-node@npm:2.1.0" @@ -10255,6 +10283,15 @@ __metadata: languageName: node linkType: hard +"domhandler@npm:^3.3.0": + version: 3.3.0 + resolution: "domhandler@npm:3.3.0" + dependencies: + domelementtype: ^2.0.1 + checksum: 850e5e9fee7834ab4314811e18bc1f4294d7eafbf6a79ad03cbe50cf964108935c97257ac248944d72a9312b4a18dfa8323e857d23278964dc83b1f124467fa3 + languageName: node + linkType: hard + "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": version: 4.3.1 resolution: "domhandler@npm:4.3.1" @@ -10283,7 +10320,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^2.5.2, domutils@npm:^2.8.0": +"domutils@npm:^2.4.2, domutils@npm:^2.5.2, domutils@npm:^2.8.0": version: 2.8.0 resolution: "domutils@npm:2.8.0" dependencies: @@ -10436,6 +10473,20 @@ __metadata: languageName: node linkType: hard +"editorconfig@npm:^1.0.3": + version: 1.0.4 + resolution: "editorconfig@npm:1.0.4" + dependencies: + "@one-ini/wasm": 0.1.1 + commander: ^10.0.0 + minimatch: 9.0.1 + semver: ^7.5.3 + bin: + editorconfig: bin/editorconfig + checksum: 09904f19381b3ddf132cea0762971aba887236f387be3540909e96b8eb9337e1793834e10f06890cd8e8e7bb1ba80cb13e7d50a863f227806c9ca74def4165fb + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -12305,7 +12356,6 @@ __metadata: "@mui/lab": ^5.0.0-alpha.148 "@mui/material": ^5.14.13 "@mui/styles": ^5.14.13 - "@reduxjs/toolkit": ^1.8.6 "@tanstack/react-query": ^4.36.1 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^13.4.0 @@ -12343,7 +12393,6 @@ __metadata: autosuggest-highlight: ^3.3.4 axios: ^1.3.4 change-case: ^4.1.2 - connected-react-router: 6.9.3 copy-to-clipboard: ^3.3.3 dayjs: ^1.11.10 enzyme: ^3.3.0 @@ -12365,7 +12414,6 @@ __metadata: notistack: ^3.0.1 passport: ^0.6.0 passport-local: ^1.0.0 - prop-types: ^15.6.2 query-string: ^6.1.0 ramda: ^0.28.0 randomcolor: ^0.5.3 @@ -12377,16 +12425,12 @@ __metadata: react-error-boundary: ^4.0.11 react-full-screen: ^0.2.2 react-i18next: ^13.2.2 - react-redux: ^8.0.4 react-router: ^6.17.0 react-router-dom: ^6.17.0 react-scripts: 5.0.1 react-transition-group: ^2.3.1 react-use-event: ^1.1.1 recoil: ^0.7.7 - redux: ^4.0.0 - redux-devtools-extension: ^2.13.5 - redux-thunk: ^2.3.0 rooks: ^7.4.1 serve: ^14.2.1 shiitake: ^3.0.2 @@ -12394,6 +12438,7 @@ __metadata: vite: ^4.4.11 vite-aliases: ^0.11.3 yup: ^1.3.2 + yup-locales: ^1.2.18 languageName: unknown linkType: soft @@ -12852,7 +12897,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0, glob@npm:^8.0.3": +"glob@npm:^8.0.0, glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -13416,6 +13461,23 @@ __metadata: languageName: node linkType: hard +"html-minifier@npm:^4.0.0": + version: 4.0.0 + resolution: "html-minifier@npm:4.0.0" + dependencies: + camel-case: ^3.0.0 + clean-css: ^4.2.1 + commander: ^2.19.0 + he: ^1.2.0 + param-case: ^2.1.1 + relateurl: ^0.2.7 + uglify-js: ^3.5.1 + bin: + html-minifier: ./cli.js + checksum: b426aee771d9da104c1c9554e3ebd3a4f483d2ce01f4dcc4156ba33a5959044acf6bea192d5ae63b290cdb92c30a9d07fd6924c65609aa82382ce411328f94ca + languageName: node + linkType: hard + "html-parse-stringify@npm:^3.0.1": version: 3.0.1 resolution: "html-parse-stringify@npm:3.0.1" @@ -13447,6 +13509,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^5.0.0": + version: 5.0.1 + resolution: "htmlparser2@npm:5.0.1" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^3.3.0 + domutils: ^2.4.2 + entities: ^2.0.0 + checksum: b67ac02e44629ec76b712fc06702451bea64e522cfcd7cc22fa85023b81b44cde5060662faa81d34f18c0fe5a43ced1cac73528d30a6df5ac5825a4d479c7ea5 + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -13790,14 +13864,14 @@ __metadata: languageName: node linkType: hard -"immer@npm:^9.0.21, immer@npm:^9.0.7": +"immer@npm:^9.0.7": version: 9.0.21 resolution: "immer@npm:9.0.21" checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 languageName: node linkType: hard -"immutable@npm:^3.8.1 || ^4.0.0, immutable@npm:^4.2.2": +"immutable@npm:^4.2.2": version: 4.3.4 resolution: "immutable@npm:4.3.4" checksum: de3edd964c394bab83432429d3fb0b4816b42f56050f2ca913ba520bd3068ec3e504230d0800332d3abc478616e8f55d3787424a90d0952e6aba864524f1afc3 @@ -15535,6 +15609,22 @@ __metadata: languageName: node linkType: hard +"js-beautify@npm:^1.6.14": + version: 1.14.9 + resolution: "js-beautify@npm:1.14.9" + dependencies: + config-chain: ^1.1.13 + editorconfig: ^1.0.3 + glob: ^8.1.0 + nopt: ^6.0.0 + bin: + css-beautify: js/bin/css-beautify.js + html-beautify: js/bin/html-beautify.js + js-beautify: js/bin/js-beautify.js + checksum: aea5af03d0e8d5bcdfc9f98d6c6ebdc17076c762123ae79557d271a921438e2c0c422bc56a955119d770bb0f01cb411003534d8ae8dc138eb7af4821f21f8352 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -15871,6 +15961,21 @@ __metadata: languageName: node linkType: hard +"juice@npm:^9.0.0": + version: 9.1.0 + resolution: "juice@npm:9.1.0" + dependencies: + cheerio: ^1.0.0-rc.12 + commander: ^6.1.0 + mensch: ^0.3.4 + slick: ^1.12.2 + web-resource-inliner: ^6.0.1 + bin: + juice: bin/juice + checksum: 95f20fa183baa17360d7f03f2699f7cbc3476fb2e3a2d1d81d28f2ce1e5cd61a634a05cad26cfe83174c730ecbde18d8db9bc244b915741833fa6ce1c61c6864 + languageName: node + linkType: hard + "jw-paginate@npm:^1.0.4": version: 1.0.4 resolution: "jw-paginate@npm:1.0.4" @@ -16350,13 +16455,6 @@ __metadata: languageName: node linkType: hard -"lodash.isequalwith@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.isequalwith@npm:4.4.0" - checksum: 428ba7a57c47ec05e2dd18c03a4b4c45dac524a46af7ce3f412594bfc7be6a5acaa51acf9ea113d0002598e9aafc6e19ee8d20bc28363145fcb4d21808c9039f - languageName: node - linkType: hard - "lodash.isfunction@npm:^3.0.9": version: 3.0.9 resolution: "lodash.isfunction@npm:3.0.9" @@ -16526,6 +16624,13 @@ __metadata: languageName: node linkType: hard +"lower-case@npm:^1.1.1": + version: 1.1.4 + resolution: "lower-case@npm:1.1.4" + checksum: 1ca9393b5eaef94a64e3f89e38b63d15bc7182a91171e6ad1550f51d710ec941540a065b274188f2e6b4576110cc2d11b50bc4bb7c603a040ddeb1db4ca95197 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -16829,6 +16934,13 @@ __metadata: languageName: node linkType: hard +"mensch@npm:^0.3.4": + version: 0.3.4 + resolution: "mensch@npm:0.3.4" + checksum: eabb25d595b9bb7c067b932ea9c96f0c8154a4bb6c454a4edecef9f5c87652e345a40128741ed95905699c5a16ad1f6c7efd5f6dfc06e18128d74b569a4fb893 + languageName: node + linkType: hard + "meow@npm:^10.1.3": version: 10.1.5 resolution: "meow@npm:10.1.5" @@ -16947,6 +17059,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^2.4.6": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 1497ba7b9f6960694268a557eae24b743fd2923da46ec392b042469f4b901721ba0adcf8b0d3c2677839d0e243b209d76e5edcbd09cfdeffa2dfb6bb4df4b862 + languageName: node + linkType: hard + "mime@npm:^3.0.0": version: 3.0.0 resolution: "mime@npm:3.0.0" @@ -17032,6 +17153,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -17168,6 +17298,408 @@ __metadata: languageName: node linkType: hard +"mjml-accordion@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-accordion@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 66212dcf89531da230115c786dec24194d8ec9a4c93bcc1cfdbac332be07678eee3b8479d46f155cb60bf13358edd5cd7e4d6538ad5f9a910cbee5bb6b450855 + languageName: node + linkType: hard + +"mjml-body@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-body@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 27388e15681bb25412a7123ae82559e6cb5586293aef3aa2cf57138bee401c1b53e84d8efacef2c9db4cb7bf8dc8cac741b7907ec11036f2b804178db511301c + languageName: node + linkType: hard + +"mjml-button@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-button@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 55fa3228476fbb17c51d63fbc9a18ce280c3246a69164bbd6d93f4670b3a9f93e985cece5958cc94ff0b60fbc199bd1382fd85d27aac0677517926ec8dd0ad6f + languageName: node + linkType: hard + +"mjml-carousel@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-carousel@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: db6d7847722ef1d4fd2b74aba04853156c729ba1a99e5565fbe5c32ed96733de1846fc41995505ec950de4953fa415586251c1e65f731725edd9d4b08b259e87 + languageName: node + linkType: hard + +"mjml-cli@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-cli@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + chokidar: ^3.0.0 + glob: ^7.1.1 + html-minifier: ^4.0.0 + js-beautify: ^1.6.14 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + yargs: ^16.1.0 + bin: + mjml-cli: bin/mjml + checksum: ed3a08c68b6c5261e173674d1f1276b2cd636f2edc8713234a071befe919f9f9aa22e254480516d4b8d49eef22989017ce4327418c1c03fe08b004b6d1f8d136 + languageName: node + linkType: hard + +"mjml-column@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-column@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: a8eb4f321b9015ba8be96d08019502ada557fe3ba55413abf71b39a9ce209d0a3550ba03714a91b03b065af8b64582f6b3703b249f77c12bc1b54a499ac10ee2 + languageName: node + linkType: hard + +"mjml-core@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-core@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + cheerio: 1.0.0-rc.12 + detect-node: ^2.0.4 + html-minifier: ^4.0.0 + js-beautify: ^1.6.14 + juice: ^9.0.0 + lodash: ^4.17.21 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + checksum: fe46769b1746b1da90ddd39c584a6c8f7db80e125e079ce83cfd8ab4888e5abfff2933f573993926b36721de194b261c28f078b9316c395b185fd4098298c025 + languageName: node + linkType: hard + +"mjml-divider@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-divider@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b44fab9de9751caf626ca6206b58c2a9ac7788c54c56d91cc892f77ed164a0fd2021422ef1019adb147a145873db499bb89f1518aa4326face1135acd8f61294 + languageName: node + linkType: hard + +"mjml-group@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-group@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 17cec7be9544ae32121cac12cf6dffd0a234bb2c14e2e72df230c52f1834f34fbd2df6ed15661491d0eee2c95dd7ad77e6048ca0ca012c9970d96bcfe8a4e77e + languageName: node + linkType: hard + +"mjml-head-attributes@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-attributes@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 93783e5ce4df95c745fee65cf2a4787eadbd548bb2d35f4c408d50cd4f81652061da4fcf54b4861db40bef115b60bb29f36faf6478033ad32e5e467415ec394a + languageName: node + linkType: hard + +"mjml-head-breakpoint@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-breakpoint@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b52ea526f9291e0919ec82a7cc89e1e4d5a22c78280bc039b97648a3b938778d3bc7ff77b658a8a5d247c80327d2677f1591a5638039edc0d7c6f86670a1aeec + languageName: node + linkType: hard + +"mjml-head-font@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-font@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 3787977d042634ed338eb5d1be8612494a55419f568187c40517d3c53d57a93d3efd13c82c89d4a5b5c6456082bee12b6f682ededbc24a071600c9986a88ee94 + languageName: node + linkType: hard + +"mjml-head-html-attributes@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-html-attributes@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: a076f05954e09d7d8d721dc6931a1ecfcfa59126d4c7859c6278404d8e036b83f8eb72fd4285f367324d170bde7df64385ddf093b9f47cf5115fffd85756a510 + languageName: node + linkType: hard + +"mjml-head-preview@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-preview@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 9d8301458a93794695a1c50a16dbdd7914c008f0a89ee87be9d83f494966fb0aa51434549a6f183a014e34bfdc23795607bc33a33a1a4225882c8d0208fa3898 + languageName: node + linkType: hard + +"mjml-head-style@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-style@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 2e96180bd72656c70507f21a37f8cf3c0dc41709052af42e1161d77551df762f62d863635c18dff6d092bab9bd8c8c631c0a09b3c6dc25575f0693ee6627b7ba + languageName: node + linkType: hard + +"mjml-head-title@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-title@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 65dfe9cb5115a9cfe76851b9e5aabaaa30131e55a4346e9ec04bde3234897ffe1ab3e7bb37a695af44deffe4a869dee34668a3d87396ed50b923310fb9baebcd + languageName: node + linkType: hard + +"mjml-head@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: c83c930badb7ad0ee5771b13928d2c371aa9b70777393e32361fa356b534d1b282f5698e41dee8f947c687d28580e80b74bac2d3308970884e58152edc86bafd + languageName: node + linkType: hard + +"mjml-hero@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-hero@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 6c6ec8e5168709f09175d030c2a6fc7326f7a2e076cf09c0676e78bd941521e2c4295335bfdce8b5c31ea946a1925edcb780aced73b0dbfda40c07a463526c93 + languageName: node + linkType: hard + +"mjml-image@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-image@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 1ad0910300b115fcc42de6d642ce35b2a3593ac1a431498a2a2f3210733ff7c2e4bc33334abbd20f9854c77aa0f7c859928941fa6cb0bce190453f857e7c7f90 + languageName: node + linkType: hard + +"mjml-migrate@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-migrate@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + js-beautify: ^1.6.14 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-parser-xml: 4.14.1 + yargs: ^16.1.0 + bin: + migrate: lib/cli.js + checksum: 6710d100d79fd0b066cfd2fd0a5f7e6d7ccbf309a31039f162a22ff7b69c0540e550325560737270b205a3a3cd4562603e6bc4a44424ca973c44168741c3f388 + languageName: node + linkType: hard + +"mjml-navbar@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-navbar@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b85ccb20a95575387b5ce65e317f9a25fe46c1d77bab506274d630950da6bcbec1034cf351887eb1aec10e6c0b8b926804fc20cbed99209de45d49ada736f969 + languageName: node + linkType: hard + +"mjml-parser-xml@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-parser-xml@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + detect-node: 2.0.4 + htmlparser2: ^8.0.1 + lodash: ^4.17.15 + checksum: 839225d2d8c5b7c8a948ebe2a49afa8aa8f4e3651810b40df95d6f39da56ea6d62e2c4e5c55f96eb60d191233c0d2c77be0ee9cc861ffa5c3da032be56e0d96c + languageName: node + linkType: hard + +"mjml-preset-core@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-preset-core@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + mjml-accordion: 4.14.1 + mjml-body: 4.14.1 + mjml-button: 4.14.1 + mjml-carousel: 4.14.1 + mjml-column: 4.14.1 + mjml-divider: 4.14.1 + mjml-group: 4.14.1 + mjml-head: 4.14.1 + mjml-head-attributes: 4.14.1 + mjml-head-breakpoint: 4.14.1 + mjml-head-font: 4.14.1 + mjml-head-html-attributes: 4.14.1 + mjml-head-preview: 4.14.1 + mjml-head-style: 4.14.1 + mjml-head-title: 4.14.1 + mjml-hero: 4.14.1 + mjml-image: 4.14.1 + mjml-navbar: 4.14.1 + mjml-raw: 4.14.1 + mjml-section: 4.14.1 + mjml-social: 4.14.1 + mjml-spacer: 4.14.1 + mjml-table: 4.14.1 + mjml-text: 4.14.1 + mjml-wrapper: 4.14.1 + checksum: 86852c543c138fcafecd461ccecd03c36b0ac573a644fe47a164b8f94465c33eee25c815e6cb17a85bd947bccd21ffb700023a22d1f39e5540ba9b663c96e7ce + languageName: node + linkType: hard + +"mjml-raw@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-raw@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 65721432a89653644ae7e451e5a09d5168e6a69900f73823b74803ac4f4ff148ee4654db916e770e9e7a4e51cb83222c95b15b0220f21d96eeb9eb1a8571be7d + languageName: node + linkType: hard + +"mjml-section@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-section@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: f4b2ba3fa916193635b273d482a23e6f2f2969d01b5517e62d505ef5b6260e404bd2df3252ebd5926c1d5dc79f33cac8ceab19c161cf8435c3a23148c0296a15 + languageName: node + linkType: hard + +"mjml-social@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-social@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 4d493dcb133beb6361cc5b6ff5799ef8456e39fd89f60d1c8ecc8767eb2fcfedf5f0a253dfafa543c6c3a32a798cb3e009b59e6588fdce5726b057435cf5d3a6 + languageName: node + linkType: hard + +"mjml-spacer@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-spacer@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 93bf08f18da4a6593ded0675d32d0b2599d8fa9b00a3f3c0d90803106611f09a48efff803f82e740e27c8e5e56a36a40c66c87045ca7090ca5685762f0fe9382 + languageName: node + linkType: hard + +"mjml-table@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-table@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: f7fc1f648a112b8bab5209e9a3200926b1c10b39acc90f691e6b2e6d75a642ebf2f8f603b72676bd3490c3afaad97d06f1a64503dd971695f431760436317b26 + languageName: node + linkType: hard + +"mjml-text@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-text@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 16133c363813a4ec5bef06fbd34789a59d06206f78c43e43f1979bb326169b9f0809c4ddf651a05ae8ea4b295dcce6ad80d6c696b628832a5357d3bb532a2d5d + languageName: node + linkType: hard + +"mjml-validator@npm:4.13.0": + version: 4.13.0 + resolution: "mjml-validator@npm:4.13.0" + dependencies: + "@babel/runtime": ^7.14.6 + checksum: 40397cc664ee0e1ad884ddef30e2ab1cb3b14bb3fb1730e9ba8d7a786c25a260726b4bb70bae7094aa4177a369fd46bd2bf7f8e744f9cdecd0c3ceb8881b075e + languageName: node + linkType: hard + +"mjml-wrapper@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-wrapper@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-section: 4.14.1 + checksum: c3421fe6d783b4dfe617b37eae21aa3ff6e345ad06e18e8aeddd91e70bea75d277004feaf39d9af298e6e3ee550553df5110121d4486e1610ad51ae61a5ddf07 + languageName: node + linkType: hard + +"mjml@npm:^4.14.1": + version: 4.14.1 + resolution: "mjml@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + mjml-cli: 4.14.1 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-preset-core: 4.14.1 + mjml-validator: 4.13.0 + bin: + mjml: bin/mjml + checksum: 48906b077ea7283f77cec0baec422ebee133a5a2ea2c727c31e35f4b4e56894ef3134fb317704c12e4bc40632321779df19b950555bc49d188675e84dca7a826 + languageName: node + linkType: hard + "mkdirp@npm:1.0.4, mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -17380,6 +17912,15 @@ __metadata: languageName: node linkType: hard +"no-case@npm:^2.2.0": + version: 2.3.2 + resolution: "no-case@npm:2.3.2" + dependencies: + lower-case: ^1.1.1 + checksum: 856487731936fef44377ca74fdc5076464aba2e0734b56a4aa2b2a23d5b154806b591b9b2465faa59bb982e2b5c9391e3685400957fb4eeb38f480525adcf3dd + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -17397,7 +17938,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": +"node-fetch@npm:2, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -17512,6 +18053,13 @@ __metadata: languageName: node linkType: hard +"nodemailer@npm:^6.9.7": + version: 6.9.7 + resolution: "nodemailer@npm:6.9.7" + checksum: 0cf66d27aed3bd2cbdff9939402cec3d2119c31b2b9ff4af3bcd59f48287ea75b90c0ce2cd9eb0df838164972cd25581b4b723c91fd673e2608bcb28445ccb1b + languageName: node + linkType: hard + "noms@npm:0.0.0": version: 0.0.0 resolution: "noms@npm:0.0.0" @@ -18301,6 +18849,15 @@ __metadata: languageName: node linkType: hard +"param-case@npm:^2.1.1": + version: 2.1.1 + resolution: "param-case@npm:2.1.1" + dependencies: + no-case: ^2.2.0 + checksum: 3a63dcb8d8dc7995a612de061afdc7bb6fe7bd0e6db994db8d4cae999ed879859fd24389090e1a0d93f4c9207ebf8c048c870f468a3f4767161753e03cb9ab58 + languageName: node + linkType: hard + "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -20119,6 +20676,13 @@ __metadata: languageName: node linkType: hard +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: 4d4826e1713cbfa0f15124ab0ae494c91b597a3c458670c9714c36e8baddf5a6aad22842776f2f5b137f259c8533e741771445eb8df82e861eea37a6eaba03f7 + languageName: node + linkType: hard + "proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -20824,7 +21388,7 @@ __metadata: languageName: node linkType: hard -"react-redux@npm:^8.0.4, react-redux@npm:^8.0.5, react-redux@npm:^8.1.1": +"react-redux@npm:^8.0.5, react-redux@npm:^8.1.1": version: 8.1.2 resolution: "react-redux@npm:8.1.2" dependencies: @@ -21270,24 +21834,6 @@ __metadata: languageName: node linkType: hard -"redux-devtools-extension@npm:^2.13.5": - version: 2.13.9 - resolution: "redux-devtools-extension@npm:2.13.9" - peerDependencies: - redux: ^3.1.0 || ^4.0.0 - checksum: 603d48fd6acf3922ef373b251ab3fdbb990035e90284191047b29d25b06ea18122bc4ef01e0704ccae495acb27ab5e47b560937e98213605dd88299470025db9 - languageName: node - linkType: hard - -"redux-thunk@npm:^2.3.0, redux-thunk@npm:^2.4.2": - version: 2.4.2 - resolution: "redux-thunk@npm:2.4.2" - peerDependencies: - redux: ^4 - checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c - languageName: node - linkType: hard - "redux@npm:^4.0.0, redux@npm:^4.2.1": version: 4.2.1 resolution: "redux@npm:4.2.1" @@ -21527,13 +22073,6 @@ __metadata: languageName: node linkType: hard -"reselect@npm:^4.1.8": - version: 4.1.8 - resolution: "reselect@npm:4.1.8" - checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e - languageName: node - linkType: hard - "resize-observer-polyfill@npm:^1.5.1": version: 1.5.1 resolution: "resize-observer-polyfill@npm:1.5.1" @@ -22124,13 +22663,6 @@ __metadata: languageName: node linkType: hard -"seamless-immutable@npm:^7.1.3": - version: 7.1.4 - resolution: "seamless-immutable@npm:7.1.4" - checksum: f65c1dc12e460265ccc4b164085b807570f9fb8a619cd3c216fc7ed933fb09c57a24a7df1b638dc9bd6367d8d69c2f00b5370b0c0996b4046242539096d2d0c6 - languageName: node - linkType: hard - "section-iterator@npm:^2.0.0": version: 2.0.0 resolution: "section-iterator@npm:2.0.0" @@ -22543,6 +23075,13 @@ __metadata: languageName: node linkType: hard +"slick@npm:^1.12.2": + version: 1.12.2 + resolution: "slick@npm:1.12.2" + checksum: 02b586dac1ce12db4e6d3b89e61962e6e07966875b8099ba1d6fac2faa8c88f37450293c27706f296556e4698b9e139bfee4055b845a7c266eca4650609d7603 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -24466,6 +25005,15 @@ __metadata: languageName: node linkType: hard +"uglify-js@npm:^3.5.1": + version: 3.17.4 + resolution: "uglify-js@npm:3.17.4" + bin: + uglifyjs: bin/uglifyjs + checksum: 7b3897df38b6fc7d7d9f4dcd658599d81aa2b1fb0d074829dd4e5290f7318dbca1f4af2f45acb833b95b1fe0ed4698662ab61b87e94328eb4c0a0d3435baf924 + languageName: node + linkType: hard + "uid-safe@npm:~2.1.5": version: 2.1.5 resolution: "uid-safe@npm:2.1.5" @@ -24723,6 +25271,13 @@ __metadata: languageName: node linkType: hard +"upper-case@npm:^1.1.1": + version: 1.1.3 + resolution: "upper-case@npm:1.1.3" + checksum: 991c845de75fa56e5ad983f15e58494dd77b77cadd79d273cc11e8da400067e9881ae1a52b312aed79b3d754496e2e0712e08d22eae799e35c7f9ba6f3d8a85d + languageName: node + linkType: hard + "upper-case@npm:^2.0.2": version: 2.0.2 resolution: "upper-case@npm:2.0.2" @@ -24937,6 +25492,13 @@ __metadata: languageName: node linkType: hard +"valid-data-url@npm:^3.0.0": + version: 3.0.1 + resolution: "valid-data-url@npm:3.0.1" + checksum: 06584294fb4c9550f0aaa56470f8d748f4ebfc3ed230707db5559754719a66fc37f299b5a79b914375b8198d90f8a51e0401375391938caf8dc8e442308aab9e + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -25268,6 +25830,20 @@ __metadata: languageName: node linkType: hard +"web-resource-inliner@npm:^6.0.1": + version: 6.0.1 + resolution: "web-resource-inliner@npm:6.0.1" + dependencies: + ansi-colors: ^4.1.1 + escape-goat: ^3.0.0 + htmlparser2: ^5.0.0 + mime: ^2.4.6 + node-fetch: ^2.6.0 + valid-data-url: ^3.0.0 + checksum: 17d9e53a6e5f07361abc584b6bb2bb8470978be580f8b5cdcab5998507ffccf5fb645616d3fe1550965d2db497f4a5cdc1ea1460c9cf464de315751962708ecc + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -26088,6 +26664,13 @@ __metadata: languageName: node linkType: hard +"yup-locales@npm:^1.2.18": + version: 1.2.18 + resolution: "yup-locales@npm:1.2.18" + checksum: e929555df880532a31973f693a0ae600522748ec08b6d0d4eff4a0d1f4af9f1d368712d74090a4b0025125e68cf95ab5d0abb9a55619f589c611181e2768dae0 + languageName: node + linkType: hard + "yup@npm:^1.3.2": version: 1.3.2 resolution: "yup@npm:1.3.2"
(type: ActionType, payload?: P): - Action
{ - return { type, payload, error: false }; -} - -export function createEmptyAction(type: ActionType): - EmptyAction { - return { type, error: false }; -} - -export function createErrorAction
(type: ActionType, payload: P): - Required> { - return { type, payload, error: true }; -} \ No newline at end of file diff --git a/apps/frontend/src/types/LevelTypes.tsx b/apps/frontend/src/types/LevelTypes.tsx deleted file mode 100644 index 6502226c..00000000 --- a/apps/frontend/src/types/LevelTypes.tsx +++ /dev/null @@ -1,34 +0,0 @@ -enum Level { - KINDERGARTEN, - ELEMENTARY_SCHOOL_1, - ELEMENTARY_SCHOOL_2, - MIDDLE_SCHOOL, - HIGH_SCHOOL, - HIGHER_EDUCATION, - RESEARCH -} - -const levelLabel = (level: Level) => { - switch (level) { - case Level.KINDERGARTEN: - return 'levels.kinderGarten'; - case Level.ELEMENTARY_SCHOOL_1: - return 'levels.elementarySchool1'; - case Level.ELEMENTARY_SCHOOL_2: - return 'levels.elementarySchool2'; - case Level.MIDDLE_SCHOOL: - return 'levels.middleSchool'; - case Level.HIGH_SCHOOL: - return 'levels.highSchool'; - case Level.HIGHER_EDUCATION: - return 'levels.higherEducation'; - case Level.RESEARCH: - return 'levels.research'; - default: - return ''; - } -}; - -const levelsCount = Object.keys(Level).length / 2; - -export { Level, levelLabel, levelsCount }; diff --git a/apps/frontend/src/types/ProjectTypes.tsx b/apps/frontend/src/types/ProjectTypes.tsx deleted file mode 100644 index 0d275cd1..00000000 --- a/apps/frontend/src/types/ProjectTypes.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export interface ProjectRouteParams { - projectId: string; -} \ No newline at end of file diff --git a/apps/frontend/src/types/StateTypes.tsx b/apps/frontend/src/types/StateTypes.tsx deleted file mode 100644 index 511e6a6f..00000000 --- a/apps/frontend/src/types/StateTypes.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - AnnotationRecord, - CommentRecord, - Credentials, - ProjectGraphRecord, - SigninErrors, - TagData, - UserRecord, -} from "@celluloid/types"; -import { RouterState } from "connected-react-router"; - -import * as SigninDialog from "~components/Signin"; - -import { PeertubeVideoInfo } from "./YoutubeTypes"; - -export interface SigninState { - loading: boolean; - dialog: SigninDialog.SigninState; - errors: SigninErrors; - credentials?: Credentials; -} - -export interface VideoState { - status: ComponentStatus; - loadingError?: boolean; - annotations: AnnotationRecord[]; - editing: boolean; - commenting: boolean; - annotationError?: string; - focusedAnnotation?: AnnotationRecord; - upsertAnnotationLoading: boolean; - deleteAnnotationLoading: boolean; - commentError?: string; - focusedComment?: CommentRecord; - upsertCommentLoading: boolean; - deleteCommentLoading: boolean; -} - -export interface ProjectDetailsState { - status: ComponentStatus; - error?: string; - project?: ProjectGraphRecord; - setPublicLoading: boolean; - setCollaborativeLoading: boolean; - unshareLoading: boolean; - deleteLoading: boolean; - setPublicError?: string; - setCollaborativeError?: string; - unshareError?: string; - deleteError?: string; -} - -export interface PlayerState { - seeking: boolean; - seekTarget: number; -} - -export interface ProjectState { - player: PlayerState; - video: VideoState; - details: ProjectDetailsState; -} - -export enum SharingStatus { - OPEN, - ERROR, - LOADING, - CLOSED, -} - -export enum ComponentStatus { - LOADING, - ERROR, - READY, -} - -export interface HomeState { - errors: { - projects?: string; - video?: string; - createProject?: string; - }; - projects: ProjectGraphRecord[]; - video?: PeertubeVideoInfo; - createProjectLoading: boolean; -} - -export interface SharingState { - status: SharingStatus; - error?: string; -} - -export interface AppState extends RouterState { - tags: TagData[]; - sharing: SharingState; - project: ProjectState; - home: HomeState; - user?: UserRecord; - signin: SigninState; - updated: boolean; -} diff --git a/apps/frontend/src/types/YoutubeTypes.tsx b/apps/frontend/src/types/YoutubeTypes.tsx deleted file mode 100644 index 12e59826..00000000 --- a/apps/frontend/src/types/YoutubeTypes.tsx +++ /dev/null @@ -1,32 +0,0 @@ -export interface PeertubeVideoInfo { - id: string; - title: string; - thumbnailUrl: string; - host:string; -} - -export interface Player { - getCurrentTime(): number; - getDuration(): number; - playVideo(): void; - pauseVideo(): void; - seekTo(position: number, allowSeekAhead: boolean): void; -} - -export interface PlayerReadyEvent { - target: Player; -} - -export interface PlayerChangeEvent { - target: Player; - data: number; -} - -export enum PlayerEventData { - UNSTARTED = -1, - ENDED = 0, - PLAYING = 1, - PAUSED = 2, - BUFFERING = 3, - CUED = 5 -} \ No newline at end of file diff --git a/apps/frontend/src/services/Constants.ts b/apps/frontend/src/utils/Constants.ts similarity index 100% rename from apps/frontend/src/services/Constants.ts rename to apps/frontend/src/utils/Constants.ts diff --git a/apps/server/package.json b/apps/server/package.json deleted file mode 100644 index ebed9540..00000000 --- a/apps/server/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "server", - "version": "2.0.0", - "description": "Celluloid backend", - "repository": "http://github.com/celluloid-camp/celluloid", - "author": "Younes Benaomar ", - "license": "MIT", - "private": true, - "dependencies": { - "bcryptjs": "^2.4.3", - "body-parser": "^1.18.2", - "change-case": "^4.1.2", - "compression": "^1.7.2", - "connect-redis": "^7.1.0", - "cookie-parser": "^1.4.3", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "express-pino-logger": "^7.0.0", - "express-session": "^1.17.3", - "extend": "^3.0.2", - "js2xmlparser": "^5.0.0", - "knex": "^2.3.0", - "lodash": "^4.17.21", - "mem": "^4.0.0", - "nodemailer": "^6.0.0", - "nodemailer-smtp-transport": "^2.7.4", - "papaparse": "^5.4.1", - "passport": "^0.6.0", - "passport-local": "^1.0.0", - "pg": "^8.8.0", - "pino": "^8.7.0", - "pino-std-serializers": "^6.0.0", - "ramda": "^0.28.0", - "redis": "^4.6.10", - "source-map-support": "^0.5.13", - "tslib": "^2.2.0", - "unfurl.js": "^6.3.1", - "validator": "^13.7.0" - }, - "scripts": { - "build": "tsup", - "dev": "dotenv -e ../../.env -- tsup --watch --silent --onSuccess 'node dist/index.js'", - "start": "node --use-strict dist/index.js" - }, - "devDependencies": { - "@celluloid/config": "*", - "@celluloid/types": "*", - "@celluloid/validators": "*", - "@types/bcrypt": "^3.0.0", - "@types/bcryptjs": "^2.4.2", - "@types/compression": "^1.0.0", - "@types/cookie-parser": "^1.4.1", - "@types/dotenv": "^6.0.0", - "@types/express": "^4.0.39", - "@types/express-pino-logger": "^4.0.2", - "@types/express-session": "^1.15.8", - "@types/knex": "^0.16.1", - "@types/node": "^18.13.0", - "@types/node-fetch": "^2.6.2", - "@types/nodemailer": "^6.4.6", - "@types/nodemailer-smtp-transport": "^2.7.4", - "@types/papaparse": "^5.3.7", - "@types/passport": "^1.0.0", - "@types/passport-local": "^1.0.33", - "@types/pg": "^7.4.1", - "@types/ramda": "^0.25.35", - "dotenv-cli": "^7.2.1", - "jest": "^27.0.1", - "knex-types": "^0.4.0", - "mock-req": "^0.2.0", - "pino-pretty": "^9.1.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" - } -} diff --git a/apps/server/src/Config.ts b/apps/server/src/Config.ts deleted file mode 100644 index 51440f5a..00000000 --- a/apps/server/src/Config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as dotEnv from 'dotenv'; - -import { envFile } from './Paths'; - -dotEnv.config({ path: envFile}); \ No newline at end of file diff --git a/apps/server/src/Paths.ts b/apps/server/src/Paths.ts deleted file mode 100644 index 26d324f6..00000000 --- a/apps/server/src/Paths.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as path from "path"; - -export const rootDir = path.resolve(__dirname, "..", "..", ".."); -export const envFile = path.resolve(rootDir, ".env"); -export const clientDir = path.resolve(rootDir, "apps", "client", "dist"); -export const publicDir = path.resolve(__dirname, "..", "public"); -export const clientApp = path.resolve(clientDir, "index.html"); diff --git a/apps/server/src/api/AnnotationApi.ts b/apps/server/src/api/AnnotationApi.ts deleted file mode 100644 index ab35d7df..00000000 --- a/apps/server/src/api/AnnotationApi.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { AnnotationData, AnnotationRecord, UserRecord } from "@celluloid/types"; -import * as express from "express"; -import Papa from 'papaparse'; - -import { isProjectOwnerOrCollaborativeMember } from "../auth/Utils"; -import { logger } from "../backends/Logger"; -import * as AnnotationStore from "../store/AnnotationStore"; -import * as CommentStore from "../store/CommentStore"; -import * as ProjectStore from "../store/ProjectStore"; -import { convertToSrt } from "../utils/srt"; -import CommentApi from "./CommentApi"; - - -const log = logger("api/AnnotationApi"); -const js2xmlparser = require("js2xmlparser"); - -const router = express.Router({ mergeParams: true }); - -router.use("/:annotationId/comments", CommentApi); - -function fetchComments(annotation: AnnotationRecord, user: UserRecord) { - return CommentStore.selectByAnnotation(annotation.id, user).then((comments) => - Promise.resolve({ ...annotation, comments } as AnnotationRecord) - ); -} - -router.get('/', (req: express.Request<{ projectId: string }>, res: express.Response) => { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - return ProjectStore.selectOne(projectId, user) - .then(() => AnnotationStore.selectByProject(projectId, user)) - .then((annotations: AnnotationRecord[]) => - Promise.all( - annotations.map((annotation) => fetchComments(annotation, user)) - ) - ) - .then((annotations) => { - return res.status(200).json(annotations); - }) - .catch((error: Error) => { - log.error("Failed to list annotations:", error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.get('/export/:format', async (req: express.Request<{ projectId: string, format: string }>, res: express.Response) => { - const projectId = req.params.projectId; - const format = req.params.format; - const user = req.user as UserRecord; - - const annotations = await AnnotationStore.selectByProject(projectId, user); - const data = await Promise.all( - annotations.map((annotation) => fetchComments(annotation, user)) - ); - - const formated = data.map((annotation) => ({ - startTime: annotation.startTime, - endTime: annotation.stopTime, - text: annotation.text, - comments: annotation.comments.map((comment) => comment.text) - })) - - let content = ""; - if (format === 'xml') { - content = js2xmlparser.parse("annotations", formated); - } else if (format == "csv") { - - content = Papa.unparse(formated); - } else if (format == "srt") { - - content = convertToSrt(formated); - - } - - res.setHeader('Content-Disposition', `attachment; filename="data.${format}"`); - res.setHeader('Content-Type', `text/${format}`); - res.send(content); - -}); - - - -router.post("/", isProjectOwnerOrCollaborativeMember, (req, res) => { - const projectId = req.params.projectId; - const annotation = req.body as AnnotationData; - const user = req.user as UserRecord; - - AnnotationStore.insert(annotation, user, projectId) - .then((result) => fetchComments(result, user)) - .then((result) => { - return res.status(201).json(result); - }) - .catch((error: Error) => { - log.error(error, "Failed to create annotation"); - return res.status(500).send(); - }); -}); - -router.put( - "/:annotationId", - isProjectOwnerOrCollaborativeMember, - (req, res) => { - const annotationId = req.params.annotationId; - const updated = req.body; - const user = req.user as UserRecord; - - AnnotationStore.selectOne(annotationId, user) - .then((old: AnnotationRecord) => - old.userId !== user.id - ? Promise.reject(new Error("UserNotAnnotationOwner")) - : Promise.resolve() - ) - .then(() => AnnotationStore.update(annotationId, updated, user)) - .then((result) => fetchComments(result, user)) - .then((result) => res.status(200).json(result)) - .catch((error: Error) => { - log.error("Failed to update annotation:", error); - if (error.message === "AnnotationNotFound") { - return res.status(404).json({ error: error.message }); - } else if (error.message === "UserNotAnnotationOwner") { - return res.status(403).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); - } -); - -router.delete( - "/:annotationId", - isProjectOwnerOrCollaborativeMember, - (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user as UserRecord; - - AnnotationStore.selectOne(annotationId, user) - .then((old: AnnotationRecord) => - old.userId !== user.id - ? Promise.reject(new Error("UserNotAnnotationOwner")) - : Promise.resolve() - ) - .then(() => AnnotationStore.del(annotationId)) - .then(() => res.status(204).send()) - .catch((error: Error) => { - log.error("Failed to delete annotation:", error); - if (error.message === "AnnotationNotFound") { - return res.status(404).json({ error: error.message }); - } else if (error.message === "UserNotAnnotationOwner") { - return res.status(403).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); - } -); - -export default router; diff --git a/apps/server/src/api/CommentApi.ts b/apps/server/src/api/CommentApi.ts deleted file mode 100644 index 60209f7d..00000000 --- a/apps/server/src/api/CommentApi.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { CommentRecord, UserRecord } from '@celluloid/types'; -import * as express from 'express'; - -import { - isLoggedIn, - isProjectOwnerOrCollaborativeMember -} from '../auth/Utils'; -import { logger } from '../backends/Logger'; -import * as AnnotationStore from '../store/AnnotationStore'; -import * as CommentStore from '../store/CommentStore'; - - -const log = logger('api/CommentApi'); - -const router = express.Router({ mergeParams: true }); - -router.get('/', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user; - - AnnotationStore.selectOne(annotationId, user) - .then(() => CommentStore.selectByAnnotation(annotationId, user as UserRecord)) - .then((comments: CommentRecord[]) => - res.status(200).json(comments)) - .catch((error: Error) => { - log.error('Failed to list comments:', error); - if (error.message === 'AnnotationNotFound') { - res.status(404).json({ error: error.message }); - } else { - res.status(500).send(); - } - }); -}); - -router.post('/', isLoggedIn, isProjectOwnerOrCollaborativeMember, async (req, res) => { - const annotationId = req.params.annotationId; - const user = req.user; - const comment = req.body.text; - - try { - await AnnotationStore.selectOne(annotationId, user); - const result = await CommentStore.insert(annotationId, comment, user as UserRecord); - - log.debug(result, "resutl"); - return res.status(201).json(result) - }catch(error){ - if (error.message === 'AnnotationNotFound') { - return res.status(404).json({ error: error.message }); - } - return res.status(500).send(); - - } -}); - -router.put('/:commentId', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req:any, res) => { - const commentId = req.params.commentId; - const updated = req.body; - const user = req.user; - - CommentStore.selectOne(commentId) - .then((old: CommentRecord) => - old.userId !== user.id - ? Promise.reject(new Error('UserNotCommentOwner')) - : Promise.resolve() - ) - .then(() => CommentStore.update(commentId, updated.text)) - .then(result => res.status(200).json(result)) - .catch((error: Error) => { - log.error('Failed to update comment:', error); - if (error.message === 'CommentNotFound') { - return res.status(404).json({ error: error.message }); - } else if (error.message === 'UserNotCommentOwner') { - return res.status(403).send({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.delete('/:commentId', isLoggedIn, isProjectOwnerOrCollaborativeMember, (req:any, res) => { - const commentId = req.params.commentId; - const user = req.user; - - CommentStore.selectOne(commentId) - .then((comment: CommentRecord) => - comment.userId !== user.id - ? Promise.reject(new Error('UserNotCommentOwner')) - : Promise.resolve() - ) - .then(() => CommentStore.del(commentId)) - .then(() => res.status(204).send()) - .catch((error: Error) => { - log.error('Failed to delete comment:', error); - if (error.message === 'CommentNotFound') { - return res.status(404).json({ error: error.message }); - } else if (error.message === 'UserNotCommentOwner') { - return res.status(403).send({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -export default router; \ No newline at end of file diff --git a/apps/server/src/api/ProjectApi.ts b/apps/server/src/api/ProjectApi.ts deleted file mode 100644 index 16f7a419..00000000 --- a/apps/server/src/api/ProjectApi.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { - ProjectCreateData, - ProjectGraphRecord, - ProjectRecord, - UserRecord, -} from "@celluloid/types"; -import { Router } from "express"; - -import { isProjectOwner, isTeacher } from "../auth/Utils"; -import { logger } from "../backends/Logger"; -import * as ProjectStore from "../store/ProjectStore"; -import AnnotationsApi from "./AnnotationApi"; - -const log = logger("api/ProjectApi"); - -const router = Router({ mergeParams: true }); - -router.use("/:projectId/annotations", AnnotationsApi); - -function fetchMembers( - project: ProjectRecord, - user?: Partial -): Promise { - if (project.collaborative || (user && user.id === project.userId)) { - return ProjectStore.selectProjectMembers(project.id); - } else if (user) { - return ProjectStore.isMember(project.id, user).then((member) => - member ? Promise.resolve([user] as UserRecord[]) : Promise.resolve([]) - ); - } else { - return Promise.resolve([]); - } -} - - - -router.get("/:projectId", (req, res) => { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - ProjectStore.selectOne(projectId, user) - .then((project: any) => { - return res.json(project); - }) - .catch((error: Error) => { - console.error(error) - log.error(`Failed to fetch project ${projectId}:`, error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.post("/", isTeacher, (req, res) => { - const user = req.user as UserRecord; - const project = req.body as ProjectCreateData; - - ProjectStore.insert(project, user) - .then((result: any) => { - return res.status(201).json(result); - }) - .catch((error: Error) => { - console.log(error); - log.error(`Failed to create project: ${JSON.stringify(error)}`); - return res.status(500).send(); - }); -}); - -router.put("/:projectId", isTeacher, isProjectOwner, (req: any, res) => { - ProjectStore.update(req.body, req.params.projectId) - .then((result) => res.status(200).json(result)) - .catch((error) => { - log.error("Failed to update project:", error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId", isTeacher, isProjectOwner, (req, res) => { - ProjectStore.del(req.params.projectId) - .then(() => res.status(204).send()) - .catch((error) => { - log.error("Failed to delete project:", error); - return res.status(500).send(); - }); -}); - -router.get("/:projectId/members", (req, res) => { - const projectId = req.params.projectId; - const user = req.user; - ProjectStore.selectOne(projectId, user as UserRecord) - .then((project: any) => fetchMembers(project, req.user)) - .then((members) => res.status(200).json(members)) - .catch((error) => { - log.error("Failed to list project members:", error); - if (error.message === "ProjectNotFound") { - return res.status(404).json({ error: error.message }); - } else { - return res.status(500).send(); - } - }); -}); - -router.put("/:projectId/share", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.shareById(projectId, req.body) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to share project with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId/share", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.unshareById(projectId) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to unshare project with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.put("/:projectId/public", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.setPublicById(projectId, true) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error(`Failed to set project public with id ${projectId}:`, error); - return res.status(500).send(); - }); -}); - -router.delete("/:projectId/public", isTeacher, isProjectOwner, (req, res) => { - const projectId = req.params.projectId; - ProjectStore.setPublicById(projectId, false) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset public on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); -}); - -router.put( - "/:projectId/collaborative", - isTeacher, - isProjectOwner, - (req, res) => { - const projectId = req.params.projectId; - return ProjectStore.setCollaborativeById(projectId, true) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset collaborative on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); - } -); - -router.delete( - "/:projectId/collaborative", - isTeacher, - isProjectOwner, - (req, res) => { - const projectId = req.params.projectId; - return ProjectStore.setCollaborativeById(projectId, false) - .then(() => ProjectStore.selectOne(projectId, req.user as UserRecord)) - .then((project) => res.status(200).json(project)) - .catch((error) => { - log.error( - `Failed to unset collaborative on project with id ${projectId}:`, - error - ); - return res.status(500).send(); - }); - } -); - -export default router; diff --git a/apps/server/src/api/TagApi.ts b/apps/server/src/api/TagApi.ts deleted file mode 100644 index 195af3a5..00000000 --- a/apps/server/src/api/TagApi.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as express from 'express'; - -import { isTeacher } from '../auth/Utils'; -import { logger } from '../backends/Logger'; -import * as TagStore from '../store/TagStore'; - -const log = logger('api/Tag'); - -const router = express.Router(); - -router.get('/', (_, res) => { - TagStore.selectAll() - .then(result => res.status(200).json(result)) - .catch(error => { - log.error('Failed to fetch tags:', error); - return res.status(500).send(); - }); -}); - -router.post('/', isTeacher, (req, res) => { - const { name } = req.body; - return TagStore.insert(name) - .then(result => - res.status(201).json(result) - ) - .catch(error => { - log.error('Failed to add new tag:', error); - return res.status(500).send(); - }); -}); - -export default router; diff --git a/apps/server/src/api/UnfurlApi.ts b/apps/server/src/api/UnfurlApi.ts deleted file mode 100644 index cdc08241..00000000 --- a/apps/server/src/api/UnfurlApi.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as express from 'express'; -import { unfurl } from 'unfurl.js' -import { URL } from 'url'; - -import { isLoggedIn } from '../auth/Utils'; -import { logger } from '../backends/Logger'; - -const log = logger('api/UnfulApi'); - -const router = express.Router(); - -type Result = { - faviconUrl: string | undefined - website: string | undefined - imageUrl: string | undefined - title: string | undefined - description: string | undefined -}; - -router.get('/', isLoggedIn, async (req, res) => { - const url = req.query.url as string; - try { - const raw = await unfurl(url); - const parsedUrl = new URL(url as string); - const result: Result = { - faviconUrl: "", - website: "", - imageUrl: undefined, - title: undefined, - description: undefined, - }; - - const ogp = raw.open_graph; - result.website = ogp.url; - - result.title = ogp.title || raw.description; - result.description = ogp.description || raw.description; - result.faviconUrl = raw.favicon - - result.imageUrl = - ogp.images && ogp.images.length > 0 ? - ogp.images[0].url : - undefined; - if (result.title && result.description) { - if (!result.website) { - result.website = parsedUrl.hostname; - } - } - return res.status(200).json(result); - - } catch (e) { - log.error(`could not unfurl link: ${e.message}`); - return res.status(500); - } - - -}); - -export default router; diff --git a/apps/server/src/api/UserApi.ts b/apps/server/src/api/UserApi.ts deleted file mode 100644 index b6939c64..00000000 --- a/apps/server/src/api/UserApi.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import { - validateConfirmResetPassword, - validateConfirmSignup, - validateLogin, - validateSignup, - validateStudentSignup, -} from "@celluloid/validators"; -import { Request, Response, Router } from "express"; -import passport from "passport"; - -import { SigninStrategy } from "../auth/Auth"; -import { - isLoggedIn, - sendConfirmationCode, - sendPasswordReset, -} from "../auth/Utils"; -import { hasConflictedOn } from "../backends/Database"; -import { logger } from "../backends/Logger"; -import * as UserStore from "../store/UserStore"; -import { TeacherServerRecord } from "../types/UserTypes"; - -const log = logger("api/User"); - -const router = Router(); - -router.post("/student-signup", (req, res, next) => { - const payload = req.body; - const result = validateStudentSignup(payload); - - - if (!result.success) { - log.error( - `Failed student signup with data ${payload}: bad request:`, - result - ); - return res.status(400).json(result); - } - return passport.authenticate(SigninStrategy.STUDENT_SIGNUP, (error: Error) => { - if (error) { - console.log("error", error) - log.error( - `Failed student signup with username ${payload.username}:`, - error - ); - if (hasConflictedOn(error, "User", "username")) { - return res.status(409).json({ - success: false, - errors: { username: "UsernameAlreadyTaken" }, - }); - } else if (error.message === "IncorrectProjectPassword") { - return res.status(403).send(); - } else { - return res.status(500).send(); - } - } else { - log.info( - `New signup for student with username ${payload.username}`, - result - ); - return res.status(201).json(result); - } - })(req, res, next); -}); - -router.post("/signup", (req, res, next) => { - const payload = req.body; - const result = validateSignup(payload); - - if (!result.success) { - log.error(`Failed user signup with data ${payload}: bad request:`, result); - return res.status(400).json(result); - } - - return passport.authenticate(SigninStrategy.TEACHER_SIGNUP, (error: Error) => { - if (error) { - log.error(`Failed user signup with email ${payload.email}:`, error); - if (hasConflictedOn(error, "User", "username")) { - return res.status(409).json({ - success: false, - errors: { username: "UsernameAlreadyTaken" }, - }); - } else if (hasConflictedOn(error, "User", "email")) { - return res.status(409).json({ - success: false, - errors: { email: "EmailAlreadyTaken" }, - }); - } else { - return res.status(500).send(); - } - } else { - log.info(`New signup from teacher with email ${payload.email}`, result); - return res.status(201).json(result); - } - })(req, res, next); -}); - -router.post("/login", (req, res, next) => { - const payload = req.body; - const result = validateLogin(req.body); - - if (!result.success) { - log.error( - `Failed user login with data ${JSON.stringify(payload)}: bad request:`, - result - ); - return res.status(400).json(result); - } - return passport.authenticate(SigninStrategy.LOGIN, (error: Error, user: Express.User) => { - if (error) { - log.error(`Failed user login with data ${payload}:`, error); - return res.status(401).json({ - success: false, - errors: { server: error.message }, - }); - } else { - return req.login(user, (err) => { - if (err) { - return res.status(500).send(); - } else { - return res.status(200).json(result); - } - }); - } - })(req, res, next); -}); - -function compareCodes(expected: string, actual: string) { - return expected.replace(/\s/g, "") === actual.replace(/\s/g, ""); -} - -router.post("/confirm-signup", (req, res) => { - const payload: any = req.body; - const result = validateConfirmSignup(payload); - - if (!result.success) { - return res.status(400).json(result); - } - return UserStore.selectOneByUsernameOrEmail(payload.login) - .then((user: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to confirm signup: user` + - ` with email ${payload.login} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - if (compareCodes(user.code || "", payload.code)) { - return UserStore.confirmByEmail(payload.login) - .then(() => res.status(200).json(result)) - .catch((error: Error) => { - log.error( - `Failed to confirm signup for user` + - ` with email ${payload.login}:`, - error - ); - return res.status(500).send(); - }); - } else { - log.error( - `Failed to confirm signup for user with email` + - ` ${payload.login}: received code ${payload.code}, expected ${user.code}` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } - } - }) - .catch((error) => { - log.error(`Failed to confirm signup:`, error); - return res.status(500).send(); - }); -}); - -router.post("/confirm-reset-password", (req, res) => { - const payload: any = req.body; - const result = validateConfirmResetPassword(payload); - - if (!result.success) { - return res.status(400).json(result); - } - return UserStore.selectOneByUsernameOrEmail(payload.login) - .then((user?: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to confirm password reset: user with email ${payload.login} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - if (compareCodes(user.code || "", payload.code)) { - return UserStore.updatePasswordByEmail( - payload.login.trim(), - payload.password - ) - .then(() => res.status(200).json(result)) - .catch((error: Error) => { - log.error( - `Failed to confirm password reset for user with email ${payload.login}`, - error - ); - return res.status(500).send(); - }); - } else { - log.error( - `Failed to confirm password reset for user with email ${payload.login}:` + - ` received code ${payload.code}, expected ${user.code}` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } - } - }) - .catch((error) => { - log.error(`Failed to confirm password reset:`, error); - return res.status(500).send(); - }); -}); - -const resendCode = - (sender: (user: TeacherRecord) => Promise) => - (req: Request, res: Response) => { - const payload = req.body; - - if (!payload.email || payload.email.trim().length === 0) { - return res.status(400).json({ - success: false, - errors: { email: "MissingEmail" }, - }); - } - return UserStore.selectOneByUsernameOrEmail(payload.email) - .then((user?: TeacherServerRecord) => { - if (!user) { - log.error( - `Failed to resend authorization code:` + - ` user with email ${payload.email} not found` - ); - return res.status(401).json({ - success: false, - errors: { server: "InvalidUser" }, - }); - } else { - return UserStore.updateCodeByEmail(payload.email).then( - (updatedUser: TeacherRecord) => - sender(updatedUser).then(() => - res.status(200).json({ success: true, errors: {} }) - ) - ); - } - }) - .catch((error: Error) => { - log.error( - `Failed to resend authorization code for user ` + - ` with email ${payload.email}`, - error - ); - return res.status(500).send(); - }); - }; - -router.post("/reset-password", (req, res) => { - return resendCode(sendPasswordReset)(req, res); -}); - -router.post("/resend-code", (req, res) => { - return resendCode(sendConfirmationCode)(req, res); -}); - -router.get("/me", isLoggedIn, (req: any, res) => { - if (req.user) { - return res.status(200).json({ - // compatibility with old frontend - teacher: { - username: req.user.username, - id: req.user.id, - role: req.user.role, - }, - username: req.user.username, - id: req.user.id, - role: req.user.role, - email: req.user.email, - }); - } else { - return res.status(401).send(); - } -}); - -router.put("/logout", isLoggedIn, (req, res) => { - if (req.session) { - req.session.destroy((err) => { - if (err) { - res.status(400).send("Unable to log out"); - } else { - res.send("Logout successful"); - } - }); - } else { - res.end(); - } -}); - -export default router; diff --git a/apps/server/src/api/VideoApi.ts b/apps/server/src/api/VideoApi.ts deleted file mode 100644 index 086bc87a..00000000 --- a/apps/server/src/api/VideoApi.ts +++ /dev/null @@ -1,67 +0,0 @@ - -import { PeerTubeVideo } from "@celluloid/types"; -import * as express from "express"; -import fetch from "node-fetch"; -import { last } from "ramda"; -import { URL } from "url"; - -import { logger } from "../backends/Logger"; - -const log = logger("api/videoApi"); - -const router = express.Router(); - -type PeerTubeVideoInfo ={ - id: string; - title: string; - thumbnailUrl: string; - host:string; -} - -async function getPeerVideoInfo(videoUrl: string): Promise { - var parsed = new URL(videoUrl); - - const host = parsed.host; - const videoId = last(parsed.pathname.split("/")); - - const url = `https://${host}/api/v1/videos/${videoId}`; - - try { - const response = await fetch(url, { - method: "GET", - headers: { - Accepts: "application/json", - }, - }); - - if (response.status === 200) { - const data:PeerTubeVideo = await response.json(); - return { - id: data.shortUUID, - host, - title: data.name, - thumbnailUrl: `https://${host}/${data.thumbnailPath}` - }; - } - log.error( - `Could not perform PeerTube API request (error ${response.status})` - ); - throw new Error("Could not perform PeerTube API request "); - } catch (e: any) { - throw new Error("Could not perform PeerTube API request "); - } -} - -router.get("/", async (req, res) => { - if (req.query.url) { - try { - const data = await getPeerVideoInfo(req.query.url as string); - return res.status(200).json(data); - } catch (e: any) { - return res.status(500); - } - } - return res.status(500); -}); - -export default router; diff --git a/apps/server/src/auth/Auth.ts b/apps/server/src/auth/Auth.ts deleted file mode 100644 index 883b6110..00000000 --- a/apps/server/src/auth/Auth.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { TeacherRecord } from "@celluloid/types"; -import bcrypt from 'bcryptjs'; -import passport from "passport"; -import { - Strategy, - VerifyFunction, - VerifyFunctionWithRequest, -} from "passport-local"; - -import { logger } from "../backends/Logger"; -import * as ProjectStore from "../store/ProjectStore"; -import * as UserStore from "../store/UserStore"; -import { TeacherServerRecord, UserServerRecord } from "../types/UserTypes"; -import { sendConfirmationCode } from "./Utils"; - -const log = logger("auth/Auth"); - - -declare global { - namespace Express { - interface User { - id: string; - } - } -} - -export enum SigninStrategy { - LOGIN = "login", - TEACHER_SIGNUP = "teacher-signup", - STUDENT_SIGNUP = "student-signup", -} - -passport.serializeUser((user, done) => { - return Promise.resolve(done(null, user.id)); -}); -passport.deserializeUser((id: string, done: any) => { - return UserStore.selectOne(id) - .then((result: TeacherRecord) => { - if (result) { - return Promise.resolve(done(null, result)); - } else { - log.error( - `Deserialize user failed: user with id` + ` ${id} does not exist` - ); - return Promise.resolve(done(new Error("InvalidUser"))); - } - }) - .catch((error: Error) => Promise.resolve(done(error))); -}); - -const signStudentUp: VerifyFunctionWithRequest = ( - req, - username, - password, - done -) => { - const { shareCode } = req.body; - - return ProjectStore.selectOneByShareName(shareCode) - .then((result) => { - if (result) { - return UserStore.createStudent(username, password, result.id); - return Promise.reject(new Error("IncorrectProjectPassword")); - } - }) - .then((user: any) => Promise.resolve(done(null, user))) - .catch((error: Error) => { - log.error("Failed to signup student:", error); - return Promise.resolve(done(error)); - }); -}; - -const signTeacherUp: VerifyFunctionWithRequest = ( - req, - email, - password, - done -) => { - return UserStore.createTeacher(req.body.username, email, password) - .then((user: TeacherServerRecord) => sendConfirmationCode(user)) - .then((user: TeacherRecord) => Promise.resolve(done(null, user))) - .catch((error: Error) => Promise.resolve(done(error))); -}; - -const logUserIn: VerifyFunction = (login, password, done) => { - return UserStore.selectOneByUsernameOrEmail(login) - .then((user: UserServerRecord) => { - if (!user) { - return Promise.resolve(done(new Error("InvalidUser"))); - } - if (!bcrypt.compareSync(password, user.password)) { - log.error(`Login failed for user ${user.username}: incorrect password`); - return Promise.resolve(done(new Error("InvalidUser"))); - } - if (!user.confirmed && user.role !== "Student") { - log.error(`Login failed: ${user.username} is not confirmed`); - return Promise.resolve(done(new Error("UserNotConfirmed"))); - } - return Promise.resolve(done(null, user)); - }) - .catch((error: Error) => Promise.resolve(done(error))); -}; - -export const loginStrategy = new Strategy( - { usernameField: "login" }, - logUserIn -); - -export const teacherSignupStrategy = new Strategy( - { passReqToCallback: true, usernameField: "email" }, - signTeacherUp -); - -export const studentSignupStrategy = new Strategy( - { passReqToCallback: true, usernameField: "username" }, - signStudentUp -); diff --git a/apps/server/src/auth/Utils.ts b/apps/server/src/auth/Utils.ts deleted file mode 100644 index 8dd7c893..00000000 --- a/apps/server/src/auth/Utils.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { UserRecord } from '@celluloid/types'; -import bcrypt from 'bcryptjs'; -import { paramCase } from 'change-case'; -import { NextFunction, Request, Response } from 'express'; - -import { sendMail } from '../backends/Email'; -import { logger } from '../backends/Logger'; -import * as ProjectStore from '../store/ProjectStore'; -import { TeacherServerRecord } from '../types/UserTypes'; - -const log = logger('auth/Auth'); - -export function hashPassword(password: string) { - const salt = bcrypt.genSaltSync(); - return bcrypt.hashSync(password, salt); -} - -export function isLoggedIn( - req: Request, - res: Response, - next: NextFunction) { - if (!req.user) { - return Promise.resolve(res.status(401).json({ - error: 'LoginRequired' - })); - } - return Promise.resolve(next()); -} - -export function isTeacher( - req: any, - res: Response, - next: NextFunction) { - if ((!req.user || req.user.role !== 'Teacher') && (!req.user || req.user.role !== 'Admin')) { - log.error('User is must be a teacher'); - return Promise.resolve(res.status(403).json({ - error: 'TeacherRoleRequired' - })); - } - return Promise.resolve(next()); -} - -export function isProjectOwner( - req: Request, - res: Response, - next: NextFunction) { - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - return ProjectStore.isOwner(projectId, user) - .then((result: boolean) => { - if (result) { - next(); - return Promise.resolve(); - } - log.error('User must be project owner'); - res.status(403).json({ - error: 'ProjectOwnershipRequired' - }); - return Promise.resolve(); - }) - .catch(error => { - log.error('Failed to check project ownership:', error); - return Promise.resolve(res.status(500).send()); - }); -} - -export function isProjectOwnerOrCollaborativeMember( - req: Request, - res: Response, - next: NextFunction) { - - const projectId = req.params.projectId; - const user = req.user as UserRecord; - - ProjectStore.isOwnerOrCollaborativeMember(projectId, user) - .then((result: boolean) => { - if (result) { - next(); - return Promise.resolve(); - } - log.error('User must be project owner or collaborator'); - res.status(403).json({ - error: 'ProjectOwnershipOrMembershipRequired' - }); - return Promise.resolve(); - }) - .catch(error => { - log.error('Failed project ownership/membership test:', error); - return Promise.resolve(res.status(500).send()); - }); -} - -export function generateConfirmationCode() { - const code = () => String(Math.floor(Math.random() * 900) + 100); - const first = code(); - const second = code(); - return `${first}${second}`; -} - -export function generateUniqueShareName(title: string, count: number) { - const compare = (a: string, b: string) => - b.length - a.length; - - const construct = (result: string[], str: string) => { - let res: string[] = [] - if (str) { - if (result.join().length < 6) { - res = [...result, str]; - } - } - return res; - }; - - const prefix = paramCase(title) - .split(/-/) - .sort(compare) - .reduce(construct, []) - .join('-'); - - return `${prefix}${count ? count : ''}`; -} - -export function sendConfirmationCode(user: TeacherServerRecord) { - const subject = `Bienvenue sur Celluloid, ${user.username} !`; - const text = - `Bonjour ${user.username},\n\n` + - `Voici votre code de confirmation : ${user.code}\n\n` + - `Ce code est valable pendant 1 heure.\n\n` + - `Veuillez le saisir dans le formulaire prévu à cet effet.\n\n` + - `L'équipe Celluloid vous souhaite la bienvenue !`; - const html = - `Bonjour ${user.username},` + - `Voici votre code de confirmation : ${user.code}` + - `Ce code est valable pendant 1 heure.` + - `Veuillez le saisir dans le formulaire prévu à cet effet.` + - `L'équipe Celluloid vous souhaite la bienvenue !`; - - return sendMail(user.email, subject, text, html).then(() => - Promise.resolve(user) - ); -} - -export function sendPasswordReset(user: TeacherServerRecord) { - const subject = `${user.username - } : réinitialisation de votre mot de passe Celluloid`; - const text = - `Bonjour ${user.username},\n\n` + - `Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}\n\n` + - `Voici votre code de confirmation: ${user.code}\n\n` + - `Ce code sera valable pendant 1 heure.\n\n` + - `Veuillez le saisir dans le formulaire prévu à cet effet.\n\n` + - `Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.\n\n` + - `Cordialement,\n\n` + - `L'équipe Celluloid`; - const html = - `Bonjour ${user.username},` + - `Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}` + - `Voici votre code de confirmation : ${user.code}` + - `Ce code sera valable pendant 1 heure.` + - `Veuillez le saisir dans le formulaire prévu à cet effet.` + - `Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.` + - `Cordialement,` + - `L'équipe Celluloid`; - - return sendMail(user.email, subject, text, html).then(() => - Promise.resolve(user) - ); -} diff --git a/apps/server/src/backends/Database.ts b/apps/server/src/backends/Database.ts deleted file mode 100644 index bf970f7e..00000000 --- a/apps/server/src/backends/Database.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Knex from "knex"; -import * as R from "ramda"; - -import configuration from "../knexfile" -import { logger } from "./Logger"; - -const log = logger("Database"); - -export const database = Knex(configuration) - - -export const filterNull = - (prop: string) => - // tslint:disable-next-line:no-any - (obj: any) => { - // tslint:disable-next-line:no-any - obj[prop] = obj[prop].filter((elem: any) => elem); - return obj; - }; - -export function getExactlyOne(rows: any[]) { - if (rows.length === 1) { - return Promise.resolve(rows[0]); - } else { - log.error("Update or insert result has less or more than one row", rows); - return Promise.reject(Error("NotExactlyOneRow")); - } -} - -const CONFLICT_ERROR = "23505"; - -interface DatabaseError extends Error { - code?: string; - constraint?: string; -} - -export function hasConflictedOn( - error: DatabaseError, - table: string, - key: string -) { - return ( - error.code && - error.constraint && - error.code === CONFLICT_ERROR && - R.equals(error.constraint.split("_"), [table, key, "key"]) - ); -} diff --git a/apps/server/src/backends/Email.ts b/apps/server/src/backends/Email.ts deleted file mode 100644 index 07068faf..00000000 --- a/apps/server/src/backends/Email.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as mailer from 'nodemailer'; -import smtp from 'nodemailer-smtp-transport'; - -import { logger } from './Logger'; - -const log = logger('Email'); - -const transport = mailer.createTransport(smtp({ - host: process.env.CELLULOID_SMTP_HOST, - port: parseInt(process.env.CELLULOID_SMTP_PORT || "465", 10), - secure: process.env.CELLULOID_SMTP_SECURE === 'true', - -})); - -export function sendMail( - to: string, subject: string, text: string, html: string) { - const mailOptions = { - from: 'Celluloid ', to, subject, text, html - }; - - return new Promise((resolve, reject) => { - transport.sendMail(mailOptions, (error, info) => { - if (error) { - log.error( - `Failed to send email to ${to} with body [${text}]`, error); - // reject(new Error('Email sending failed')); - resolve(null); - } else { - log.info( - `Email sent to ${to} with subject [${subject}]`, info.response); - resolve(null); - } - }); - }); -} diff --git a/apps/server/src/backends/Logger.ts b/apps/server/src/backends/Logger.ts deleted file mode 100644 index 25312d0f..00000000 --- a/apps/server/src/backends/Logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import pino from 'pino'; - -export const Logger = pino({ - level: process.env.CELLULOID_LOG_LEVEL || 'info' -}); - -export const logger = (module: string) => Logger.child({ module }); \ No newline at end of file diff --git a/apps/server/src/database/connection.ts b/apps/server/src/database/connection.ts deleted file mode 100644 index 392269ed..00000000 --- a/apps/server/src/database/connection.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Knex from "knex"; -import configuration from "../knexfile"; -export const knex = Knex(configuration); \ No newline at end of file diff --git a/apps/server/src/http/SessionStore.ts b/apps/server/src/http/SessionStore.ts deleted file mode 100644 index 4038cb12..00000000 --- a/apps/server/src/http/SessionStore.ts +++ /dev/null @@ -1,34 +0,0 @@ -import RedisStore from "connect-redis" -import session from "express-session"; -import { createClient } from "redis"; - -import { logger } from "../backends/Logger"; - -const log = logger("http/Session"); - -export function createSession() { - const redisClient = createClient({ url: process.env.REDIS_URL || "redis://localhost" }); - redisClient.connect().catch((e) => log.error(`redis error : ${e.message}`)); - - const redisStore = new RedisStore({ - client: redisClient, - }) - log.info("redis connected"); - return session({ - store: redisStore, - name: process.env.CELLULOID_COOKIE_NAME - ? process.env.CELLULOID_COOKIE_NAME - : undefined, - cookie: { - domain: process.env.CELLULOID_COOKIE_DOMAIN - ? process.env.CELLULOID_COOKIE_DOMAIN - : undefined, - secure: process.env.CELLULOID_COOKIE_SECURE === "true", - maxAge: 30 * 24 * 3600 * 1000, - httpOnly: true, - }, - secret: process.env.CELLULOID_COOKIE_SECRET as string, - resave: false, - saveUninitialized: true, - }); -} diff --git a/apps/server/src/index.d.ts b/apps/server/src/index.d.ts deleted file mode 100644 index ca88d8ee..00000000 --- a/apps/server/src/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare global { - namespace Express { - interface User { - id: string; - } - - // These open interfaces may be extended in an application-specific manner via declaration merging. - // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts) - interface Request { - user?: { - id?: string; - }; - } - interface Response {} - interface Application {} - } -} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts deleted file mode 100644 index 4834fbe9..00000000 --- a/apps/server/src/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import "./Config"; - -import bodyParser from "body-parser"; -import compression from "compression"; -import express from "express"; -// import expressPino from "express-pino-logger"; -import passport from "passport"; - -import ProjectsApi from "./api/ProjectApi"; -import TagsApi from "./api/TagApi"; -import UnfurlApi from "./api/UnfurlApi"; -import UsersApi from "./api/UserApi"; -import VideosApi from "./api/VideoApi"; -import { - loginStrategy, - SigninStrategy, - studentSignupStrategy, - teacherSignupStrategy, -} from "./auth/Auth"; -import { logger } from "./backends/Logger"; -import { createSession } from "./http/SessionStore"; -const packageJson = require('../package.json'); - -require("cookie-parser"); - -const log = logger("http"); - -passport.use(SigninStrategy.LOGIN, loginStrategy); -passport.use(SigninStrategy.TEACHER_SIGNUP, teacherSignupStrategy); -passport.use(SigninStrategy.STUDENT_SIGNUP, studentSignupStrategy); -const app = express(); -app.enable('trust proxy'); -// app.use(express.static(publicDir)); -// app.use(express.static(clientDir)); -app.use(bodyParser.json()); -app.use(compression()); -app.use(createSession()); -// app.use(expressPino({ logger: log })); -app.use(passport.initialize()); -app.use(passport.session()); - -app.get("/api/status", (_, res) => res.status(200).json({ - commit: process.env.COMMIT, - version: packageJson.version -})); - -app.use("/api/projects", ProjectsApi); -app.use("/api/users", UsersApi); -app.use("/api/tags", TagsApi); -app.use("/api/unfurl", UnfurlApi); -app.use("/api/video", VideosApi); - - -// app.get("/*", (_, res) => res.sendFile(clientApp)); - - -(async () => { - try { - app.listen(process.env.CELLULOID_LISTEN_PORT, () => { - log.info( - `HTTP server listening on port ${process.env.CELLULOID_LISTEN_PORT}` + - ` in ${process.env.NODE_ENV} mode` - ); - }); - } catch (err) { - log.error(err); - } -})(); diff --git a/apps/server/src/knex/index.ts b/apps/server/src/knex/index.ts deleted file mode 100644 index 1976daeb..00000000 --- a/apps/server/src/knex/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from "./types"; - -import { Knex } from "knex"; -import { Annotation, Comment, Project, User } from "./types"; - -declare module "knex/types/tables" { - interface Tables { - // This is same as specifying `knex('users')` - users: User; - annotations: Annotation; - comments: Comment; - projects: Project; - } -} diff --git a/apps/server/src/knex/types.ts b/apps/server/src/knex/types.ts deleted file mode 100644 index 6b0fb885..00000000 --- a/apps/server/src/knex/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -// The TypeScript definitions below are automatically generated. -// Do not touch them, or risk, your modifications being lost. - -export enum UserRole { - Admin = "Admin", - Teacher = "Teacher", - Student = "Student", -} - -export enum Table { - Annotation = "Annotation", - Comment = "Comment", - Language = "Language", - Project = "Project", - Session = "Session", - Tag = "Tag", - TagToProject = "TagToProject", - User = "User", - UserToProject = "UserToProject", -} - -export type Annotation = { - id: string; - text: string; - startTime: number; - stopTime: number; - pause: boolean; - userId: string; - projectId: string; -}; - -export type Comment = { - id: string; - annotationId: string; - userId: string; - text: string; - createdAt: Date; -}; - -export type Language = { - id: string; - name: string | null; -}; - -export type Project = { - id: string; - videoId: string; - title: string; - description: string; - assignments: string[] | null; - publishedAt: Date; - objective: string; - levelStart: number; - levelEnd: number; - public: boolean; - collaborative: boolean; - userId: string; - shared: boolean; - shareName: string | null; - shareExpiresAt: Date | null; - sharePassword: string | null; - host: string; -}; - -export type Session = { - sid: string; - session: string; - expiresAt: Date; -}; - -export type Tag = { - id: string; - name: string; - featured: boolean; -}; - -export type TagToProject = { - tagId: string; - projectId: string; -}; - -export type User = { - id: string; - email: string | null; - password: string; - confirmed: boolean; - code: string | null; - codeGeneratedAt: Date | null; - username: string; - role: UserRole; -}; - -export type UserToProject = { - userId: string; - projectId: string; -}; - diff --git a/apps/server/src/knexfile.ts b/apps/server/src/knexfile.ts deleted file mode 100644 index a351a9fd..00000000 --- a/apps/server/src/knexfile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as dotEnv from 'dotenv'; -import { Knex } from 'knex'; - -dotEnv.config({ path: '../../.env' }); - - -const configuration = { - client: "pg", - connection: { - host: process.env.CELLULOID_PG_HOST, - database: process.env.CELLULOID_PG_DATABASE, - user: process.env.CELLULOID_PG_USER, - password: process.env.CELLULOID_PG_PASSWORD - }, - pool: { - min: 2, - max: 10 - } -} as Knex.Config - -export default configuration; diff --git a/apps/server/src/middleware/installDatabase.ts b/apps/server/src/middleware/installDatabase.ts deleted file mode 100644 index 0922a505..00000000 --- a/apps/server/src/middleware/installDatabase.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import { Express } from "express"; -import Knex from "knex"; -import configuration from "../knexfile"; -export const knex = Knex(configuration); - -export default (app: Express) => { - - app.set("knex", knex); - - // const shutdownActions = getShutdownActions(app); - // shutdownActions.push(() => { - // rootPgPool.end(); - // }); - - }; \ No newline at end of file diff --git a/apps/server/src/migrations/20221107103108_initial.ts b/apps/server/src/migrations/20221107103108_initial.ts deleted file mode 100644 index 7dc38604..00000000 --- a/apps/server/src/migrations/20221107103108_initial.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.raw('CREATE EXTENSION IF NOT EXISTS "pgcrypto"'); - await knex.raw('CREATE EXTENSION IF NOT EXISTS "plpgsql"'); - await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); - - if (!(await knex.schema.hasTable("User"))) { - await knex.schema.createTable("User", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.string("email").notNullable().unique(); - table.string("username").notNullable().unique(); - table.string("password").notNullable(); - table.boolean("confirmed").notNullable().defaultTo(false); - table.text("code"); - table.timestamp("codeGeneratedAt"); - table - .enu("role", ["Admin", "Teacher", "Student"], { - useNative: true, - enumName: "UserRole", - }) - .checkIn(["Teacher", "Admin"]); - table.jsonb("extra").defaultTo({}); - }); - - knex.schema.raw(` - ALTER TABLE - User - ADD CONSTRAINT - Project_check_userValid - CHECK - ((((role = ANY (ARRAY['Teacher'::public."UserRole", 'Admin'::public."UserRole"])) AND (email IS NOT NULL)) OR ((role = 'Student'::public."UserRole")))) - `); - } - - await knex.schema.createTable("Language", (table) => { - table.text("id").notNullable().unique(); - table.text("name").notNullable(); - }); - - await knex.schema.createTable("Project", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("videoId").notNullable(); - - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - - table.text("title").notNullable(); - table.text("description").notNullable(); - table.text("host"); - table.specificType("assignments", "text[]"); - table.timestamp("publishedAt").notNullable().defaultTo(knex.fn.now()); - table.text("objective").notNullable(); - table.integer("levelStart").notNullable(); - table.integer("levelEnd").notNullable(); - table.boolean("public").notNullable().defaultTo(false); - table.boolean("collaborative").notNullable(); - table.boolean("shared").notNullable().defaultTo(false); - table.text("shareName").unique(); - table.timestamp("shareExpiresAt"), table.text("sharePassword"); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Annotation", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - - table.text("text").notNullable(); - table.float("startTime").notNullable(); - table.float("stopTime").notNullable(); - table.boolean("pause").notNullable(); - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - table - .uuid("projectId") - .notNullable() - .references("Project.id") - .onDelete("CASCADE"); - table.timestamp("createdAt").defaultTo(knex.fn.now()); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Comment", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("text").notNullable(); - table - .uuid("annotationId") - .notNullable() - .references("Annotation.id") - .onDelete("CASCADE"); - table - .uuid("userId") - .notNullable() - .references("User.id") - .onDelete("CASCADE"); - table.timestamp("createdAt").defaultTo(knex.fn.now()); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("Tag", (table) => { - table - .uuid("id") - .unique() - .notNullable() - .primary() - .defaultTo(knex.raw("uuid_generate_v4()")); - table.text("name").notNullable(); - table.boolean("featured").notNullable().defaultTo(false); - table.jsonb("extra").defaultTo({}); - }); - - await knex.schema.createTable("TagToProject", (table) => { - table.uuid("tagId").notNullable().references("Tag.id").onDelete("CASCADE"); - table - .uuid("projectId") - .notNullable() - .references("Project.id") - .onDelete("CASCADE"); - table.unique(["tagId", "projectId"], { - indexName: "TagToProjectTagIdProjectIdUnique", - }); - }); - - await knex.schema.createTable("UserToProject", (table) => { - table.uuid("userId").references("User.id").onDelete("CASCADE"); - table.uuid("projectId").references("Project.id").onDelete("CASCADE"); - }); - // } -} - -exports.down = function (knex: Knex): Promise { - throw new Error("Enable to rollback, please use backup"); -}; diff --git a/apps/server/src/migrations/20230117091650_fix-role.ts b/apps/server/src/migrations/20230117091650_fix-role.ts deleted file mode 100644 index 14197715..00000000 --- a/apps/server/src/migrations/20230117091650_fix-role.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.raw( - `ALTER TABLE public."User" DROP CONSTRAINT IF EXISTS "User_role_check"; - ALTER TABLE public."User" ALTER COLUMN email DROP NOT NULL;` - ); -} - -export async function down(): Promise { - return; -} diff --git a/apps/server/src/seeds/default_tags.ts b/apps/server/src/seeds/default_tags.ts deleted file mode 100644 index 4b23c4a1..00000000 --- a/apps/server/src/seeds/default_tags.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Knex } from "knex"; - -const records = [ - { - id: "58d5d8e2-63fd-4f71-993a-642537afe905", - name: "Français - Lettres", - featured: true, - }, - { - id: "b00273d0-f65e-4115-807b-4d6eff189b43", - name: "Mathématiques", - featured: true, - }, - { - id: "f908ead8-f15c-4f5f-84c3-4dfb7e31f1d3", - name: "Histoire", - featured: true, - }, - { - id: "05639d30-37e5-4280-8bcb-092f39c28819", - name: "Géographie", - featured: true, - }, - { - id: "2dc18987-a44f-4b5f-9c0d-be81042b767b", - name: "Technologie", - featured: true, - }, - { - id: "c64c3545-096d-4cb6-9df8-4600aac715bc", - name: "Ëducation civique", - featured: true, - }, - { - id: "553f4da0-5f1d-4ec0-aafb-7dc8adb109e6", - name: "Sciences Physiques", - featured: true, - }, - { - id: "45cf959a-69c5-451a-b185-ef16f2344d7d", - name: "Sport", - featured: true, - }, - { - id: "27eba991-e805-4a82-8281-618b1236380d", - name: "Sciences de la Vie", - featured: true, - }, - { - id: "5a93968a-9047-4d80-a601-539e8393a4cb", - name: "Langues", - featured: true, - }, - { - id: "67b3121e-6893-4ed3-b2df-5318e9bfda5c", - name: "Musique", - featured: true, - }, - { - id: "fbd709d6-68ff-4540-800e-8649bec88892", - name: "Arts", - featured: true, - }, - { - id: "4fa36e4b-b9ea-42ee-8c7a-cf27fa7292eb", - name: "Economie", - featured: true, - }, - { - id: "fefba9b8-32c4-41a7-a3ec-1d810b42d843", - name: "Philosophie", - featured: true, - }, - { - id: "e61332fa-44e5-4719-9e8a-3a62848c44dd", - name: "Projets de recherche", - featured: true, - }, -]; - -export async function seed(knex: Knex): Promise { - await knex.transaction((trx) => { - return trx("Tag").insert(records).onConflict("id").merge(["name", "featured"]) - }); -} diff --git a/apps/server/src/store/AnnotationStore.ts b/apps/server/src/store/AnnotationStore.ts deleted file mode 100644 index 131277da..00000000 --- a/apps/server/src/store/AnnotationStore.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { AnnotationData, AnnotationRecord, UserRecord } from "@celluloid/types"; -import { Knex } from "knex"; - -import { database, getExactlyOne } from "../backends/Database"; -import * as ProjectStore from "./ProjectStore"; - -export function selectByProject(projectId: string, user?: UserRecord) { - return database - .select( - database.raw('"Annotation".*'), - database.raw( - "json_build_object(" + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"' - ) - ) - .from("Annotation") - .innerJoin("Project", "Project.id", "Annotation.projectId") - .innerJoin("User", "User.id", "Annotation.userId") - .where("Annotation.projectId", projectId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .orderBy("Annotation.startTime", "asc"); -} - -export function selectOne( - annotation: string | { id: string }, - user?: Partial -) { - let annotationId = annotation; - - if (typeof annotation === "object") { - annotationId = annotation.id; - } - - return database - .select( - database.raw('"Annotation".*'), - database.raw( - "json_build_object(" + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"' - ) - ) - .from("Annotation") - .innerJoin("Project", "Project.id", "Annotation.projectId") - .innerJoin("User", "User.id", "Annotation.userId") - .where("Annotation.id", annotationId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .first() - .then((row?: AnnotationRecord) => - row - ? Promise.resolve(row) - : Promise.reject(new Error("AnnotationNotFound")) - ); -} - -export function insert( - annotation: AnnotationData, - user: UserRecord, - projectId: string -) { - return database("Annotation") - .insert({ - text: annotation.text, - startTime: annotation.startTime, - stopTime: annotation.stopTime, - pause: annotation.pause, - userId: user.id, - projectId: projectId, - }) - .returning("id") - .then(getExactlyOne) - .then((id) => selectOne(id, user)); -} - -export function update(id: string, data: AnnotationData, user: UserRecord) { - return database("Annotation") - .update({ - text: data.text, - startTime: data.startTime, - stopTime: data.stopTime, - pause: data.pause, - }) - .returning("id") - .where("id", id) - .then(getExactlyOne) - .then(() => selectOne(id, user)); -} - -export function del(id: string) { - return database("Annotation").where("id", id).del(); -} diff --git a/apps/server/src/store/CommentStore.ts b/apps/server/src/store/CommentStore.ts deleted file mode 100644 index a57b0901..00000000 --- a/apps/server/src/store/CommentStore.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { CommentRecord, UserRecord } from '@celluloid/types'; -import { Knex } from 'knex'; - -import { database, getExactlyOne } from '../backends/Database'; -import { Logger } from '../backends/Logger'; -import * as ProjectStore from './ProjectStore'; - -export function selectByAnnotation(annotationId: string, user: Partial) { - return database.select( - database.raw('"Comment".*'), - database.raw( - 'json_build_object(' + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"')) - .from('Comment') - .innerJoin('Annotation', 'Annotation.id', 'Comment.annotationId') - .innerJoin('User', 'User.id', 'Comment.userId') - .innerJoin('Project', 'Project.id', 'Annotation.projectId') - .where('Comment.annotationId', annotationId) - .andWhere((nested: Knex.QueryBuilder) => { - nested.where('User.id', user.id); - nested.modify(ProjectStore.orIsOwner, user); - nested.modify(ProjectStore.orIsMember, user); - return nested; - }) - .orderBy('Comment.createdAt', 'asc'); -} - -export function selectOne(commentId: string) { - - console.log(commentId, "selectOne") - return database.select( - database.raw('"Comment".*'), - database.raw( - 'json_build_object(' + - `'id', "User"."id",` + - `'email', "User"."email",` + - `'username', "User"."username",` + - `'role', "User"."role"` + - ') as "user"')) - .from('Comment') - .innerJoin('User', 'User.id', 'Comment.userId') - .where('Comment.id', commentId) - .first() - .then((row?: CommentRecord) => row ? Promise.resolve(row) : - Promise.reject(new Error('CommentNotFound'))); -} - -export function insert(annotationId: string, text: string, user: Partial) { - return database('Comment') - .insert({ - annotationId, - userId: user.id, - text, - createdAt: database.raw('NOW()') - }) - .returning('id') - .then(getExactlyOne) - .then(row => selectOne(row.id)); -} - -export function update(id: string, text: string) { - return database('Comment') - .update({ - text - }) - .where('id', id) - .returning('id') - .then(getExactlyOne) - .then(() => selectOne(id)); -} - -export function del(id: string) { - return database('Comment') - .where('id', id) - .del(); -} \ No newline at end of file diff --git a/apps/server/src/store/ProjectStore.ts b/apps/server/src/store/ProjectStore.ts deleted file mode 100644 index b01082d4..00000000 --- a/apps/server/src/store/ProjectStore.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - ProjectCreateData, - ProjectRecord, - ProjectShareData, - UserRecord, -} from "@celluloid/types"; -import { Knex } from "knex"; - -import { generateUniqueShareName } from "../auth/Utils"; -import { - database, - filterNull, - getExactlyOne, - hasConflictedOn, -} from "../backends/Database"; -import { logger } from "../backends/Logger"; -import { Project, User } from "../knex"; -import { tagProject } from "./TagStore"; - -const log = logger("store/ProjectStore"); - -export const orIsMember = (nested: Knex.QueryBuilder, user?: UserRecord) => - user - ? nested.orWhereIn( - "Project.id", - database - .select("projectId") - .from("UserToProject") - .where("userId", user.id) - ) - : nested; - -export const orIsOwner = (nested: Knex.QueryBuilder, user?: UserRecord) => - user ? nested.orWhere("Project.userId", user.id) : nested; - -function filterUserProps({ id, username, role }: UserRecord) { - return { - id, - username, - role, - }; -} - -export function isOwnerOrCollaborativeMember( - projectId: string, - user: UserRecord -) { - return Promise.all([ - isOwner(projectId, user), - isCollaborativeMember(projectId, user), - ]).then(([owner, member]: boolean[]) => owner || member); -} - -export function isOwner(projectId: string, user: UserRecord) { - return database - .first("id") - .from("Project") - .where("id", projectId) - .andWhere("userId", user.id) - .then((row: string) => (row ? true : false)); -} - -export function isMember(projectId: string, user: Partial) { - return ( - database - .first("projectId") - .from("UserToProject") - .where("UserToProject.projectId", projectId) - // @ts-ignore - .andWhere("UserToProject.userId", user.id) - .then((row: string) => (row ? true : false)) - ); -} - -export function isCollaborativeMember(projectId: string, user: UserRecord) { - return database - .first("projectId") - .from("UserToProject") - .innerJoin("Project", "Project.id", "UserToProject.projectId") - .where("UserToProject.projectId", projectId) - .andWhere("UserToProject.userId", user.id) - .andWhere("Project.collaborative", true) - .then((row: string) => (row ? true : false)); -} - -// < Project[] &{ -// tags: Tag[], -// user: User -// }> -export function selectAll(user: UserRecord): Promise { - return database("projects") - .select( - database.raw('"Project".*'), - // database.raw(`to_json(array_agg("Tag")) AS "tags"`), - database.raw(`row_to_json("User") as "user"`) - ) - .from("Project") - .innerJoin("User", "User.id", "Project.userId") - // .leftJoin("TagToProject", "Project.id", "TagToProject.projectId") - // .leftJoin("Tag", "Tag.id", "TagToProject.tagId") - .where("Project.public", true) - .modify(orIsOwner, user) - .modify(orIsMember, user) - .groupBy("Project.id", "User.id") - .then((rows) => - rows.map((r: any) => ({ - ...r, - user: filterUserProps(r.user), - }) - ) - ); -} - - - -export function selectOneByShareName(shareCode: string) { - return database.first("*").from("Project").where("shareCode", shareCode); -} - -export function selectOne(projectId: string, user: Partial) { - return database - .first( - database.raw('"Project".*'), - // database.raw(`to_json(array_agg("Tag")) as "tags"`), - database.raw(`row_to_json("User") as "user"`) - ) - .from("Project") - .innerJoin("User", "User.id", "Project.userId") - // .leftJoin("TagToProject", "Project.id", "TagToProject.projectId") - // .leftJoin("Tag", "Tag.id", "TagToProject.tagId") - .where((nested: Knex.QueryBuilder) => { - nested.where("Project.public", true); - nested.modify(orIsMember, user); - nested.modify(orIsOwner, user); - }) - .andWhere("Project.id", projectId) - .groupBy("Project.id", "User.id") - .then((row?) => { - return new Promise((resolve, reject) => { - if (row) { - return selectProjectMembers(projectId).then((members) => - resolve( - { - user: filterUserProps(row.user), - members, - ...row, - } - ) - ); - } else { - return reject(new Error("ProjectNotFound")); - } - }); - }); -} - -export function insert(project: ProjectCreateData, user: UserRecord) { - const INSERT_RETRY_COUNT = 20; - const { tags, ...props } = project; - const query: any = (retry: number) => - database("Project") - .insert({ - ...props, - userId: user.id, - publishedAt: database.raw("NOW()"), - shareName: generateUniqueShareName(props.title, retry), - }) - .returning("*") - .then(getExactlyOne) - .catch((error) => { - if (hasConflictedOn(error, "User", "username")) { - if (retry < INSERT_RETRY_COUNT) { - return query(retry + 1); - } else { - log.warn( - "Failed to insert project: unique share name generation failed" - ); - } - } - throw error; - }); - return query(0).then((record: any) => - Promise.all(project.tags.map((tag) => tagProject(tag.id, record.id))).then( - () => Promise.resolve({ tags, ...record }) - ) - ); -} - -export function update(projectId: string, props: ProjectRecord) { - return database("Project") - .update(props) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function del(projectId: string) { - return database("Project").where("id", projectId).del(); -} - -export function shareById(projectId: string, data: ProjectShareData) { - return database("Project") - .update({ - shared: true, - sharePassword: data.sharePassword, - }) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function unshareById(projectId: string) { - return database("Project") - .update({ - shared: false, - sharePassword: null, - }) - .returning("*") - .where("id", projectId) - .then(getExactlyOne); -} - -export function selectProjectMembers(projectId: string) { - return database - .select("User.id", "User.username", "User.role") - .from("UserToProject") - .innerJoin("User", "User.id", "UserToProject.userId") - .where("UserToProject.projectId", projectId) - .then((rows) => rows.map(filterUserProps)); -} - -export function setPublicById(projectId: string, _public: boolean) { - return database("Project") - .update({ - public: _public, - }) - .where("id", projectId) - .returning("*") - .then(getExactlyOne); -} - -export function setCollaborativeById( - projectId: string, - collaborative: boolean -) { - return database("Project") - .update({ - collaborative, - }) - .where("id", projectId) - .returning("*") - .then(getExactlyOne); -} - diff --git a/apps/server/src/store/TagStore.ts b/apps/server/src/store/TagStore.ts deleted file mode 100644 index 5cca7bba..00000000 --- a/apps/server/src/store/TagStore.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { database, getExactlyOne } from '../backends/Database'; - -export function selectAll() { - return database.select() - .from('Tag'); -} - -export function insert(name: string) { - return database('Tag') - .insert({ - 'name': name, - 'featured': false - }) - .returning('*') - .then(getExactlyOne); -} - -export function tagProject(tagId: string, projectId: string) { - return database('TagToProject') - .insert({ - tagId, - projectId - }); -} - -export function untagProject(tagId: string, projectId: string) { - return database('TagToProject') - .insert({ - tagId, - projectId - }); -} \ No newline at end of file diff --git a/apps/server/src/store/UserStore.ts b/apps/server/src/store/UserStore.ts deleted file mode 100644 index ed2c0d11..00000000 --- a/apps/server/src/store/UserStore.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { UserRecord } from "@celluloid/types"; -import { Knex } from "knex"; - -import { generateConfirmationCode, hashPassword } from "../auth/Utils"; -import { database, getExactlyOne } from "../backends/Database"; - -export function createStudent( - username: string, - password: string, - projectId: string -) { - - - return database.transaction((transaction) => - database("User") - .transacting(transaction) - .insert({ - password: hashPassword(password), - username, - confirmed: false, - role: "Student", - }) - .returning("*") - .then(getExactlyOne) - .then((student) => - joinProject(student.id, projectId, transaction).then(() => - Promise.resolve(student) - ) - ) - .then(transaction.commit) - .catch(transaction.rollback) - ); -} - -export function createTeacher( - username: string, - email: string, - password: string -) { - return database("User") - .insert({ - email, - password: hashPassword(password), - username, - code: generateConfirmationCode(), - codeGeneratedAt: database.raw("NOW()"), - confirmed: false, - role: "Teacher", - }) - .returning("*") - .then(getExactlyOne); -} - -export function updatePasswordByEmail(login: string, password: string) { - return database("User") - .update({ - password: hashPassword(password), - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function updateCodeByEmail(login: string) { - return database("User") - .update({ - code: generateConfirmationCode(), - codeGeneratedAt: database.raw("NOW()"), - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function confirmByEmail(login: string) { - return database("User") - .update({ - code: null, - codeGeneratedAt: null, - confirmed: true, - }) - .where("email", login) - .orWhere("username", login) - .returning("*") - .then(getExactlyOne); -} - -export function selectOne(id: string) { - return database("User").first().where("id", id); -} - -export function selectOneByUsernameOrEmail(login: string) { - return database("User") - .first() - .where("username", login) - .orWhere("email", login); -} - -function withTransaction( - query: Knex.QueryBuilder, - transaction?: Knex.Transaction -) { - return transaction ? query.transacting(transaction) : query; -} - -export function joinProject( - userId: string, - projectId: string, - transaction?: Knex.Transaction -) { - return withTransaction(database("UserToProject"), transaction).insert({ - userId, - projectId, - }); -} - -export function leaveProject(userId: string, projectId: string) { - return database("UserToProject") - .where("userId", userId) - .andWhere("projectId", projectId) - .del(); -} diff --git a/apps/server/src/types/UserTypes.ts b/apps/server/src/types/UserTypes.ts deleted file mode 100644 index 015754ec..00000000 --- a/apps/server/src/types/UserTypes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - UserRecord, -} from '@celluloid/types'; - -export interface UserServerRecord extends UserRecord { - confirmed: boolean; - password: string; -} - -export interface TeacherServerRecord extends UserServerRecord { - code?: string; - codeExpiresAt?: Date; - email: string; -} - -export interface AdminServerRecord extends UserServerRecord { - code: string; - codeExpiresAt: Date; - email: string; -} diff --git a/apps/server/src/utils/generate-types.ts b/apps/server/src/utils/generate-types.ts deleted file mode 100644 index ff1b8942..00000000 --- a/apps/server/src/utils/generate-types.ts +++ /dev/null @@ -1,9 +0,0 @@ -const { knex } = require("knex"); -const { updateTypes } = require("knex-types"); - -const db = knex(require("../knexfile").development); - -updateTypes(db, { output: "../types.ts" }).catch((err:any) => { - console.error(err); - process.exit(1); -}); \ No newline at end of file diff --git a/apps/server/src/utils/srt.ts b/apps/server/src/utils/srt.ts deleted file mode 100644 index 4e2f1b35..00000000 --- a/apps/server/src/utils/srt.ts +++ /dev/null @@ -1,35 +0,0 @@ -interface Subtitle { - startTime: number; - endTime: number; - text: string; -} - -export function convertToSrt(json: Subtitle[]): string { - let srt = ''; - - json.forEach((subtitle: Subtitle, index: number) => { - const { startTime, endTime, text } = subtitle; - - // Format the start and end time in HH:MM:SS,mmm format - const formattedStartTime = formatTime(startTime); - const formattedEndTime = formatTime(endTime); - - // Add the subtitle index, start and end time, and text to the SRT format - srt += `${index + 1}\n${formattedStartTime} --> ${formattedEndTime}\n${text}\n\n`; - }); - - return srt; -} - -function formatTime(time: number): string { - const hours = Math.floor(time / 3600); - const minutes = Math.floor((time % 3600) / 60); - const seconds = Math.floor(time % 60); - const milliseconds = Math.round((time % 1) * 1000); - - return `${padNumber(hours)}:${padNumber(minutes)}:${padNumber(seconds)},${padNumber(milliseconds, 3)}`; -} - -function padNumber(number: number, length = 2): string { - return number.toString().padStart(length, '0'); -} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json deleted file mode 100644 index 5e89adc9..00000000 --- a/apps/server/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "@celluloid/config/tsconfig/node16.json", - "compilerOptions": { - "paths": { - /* IMPORTANT: this must be the same of 'src/aliases.ts' */ - "~/*": [ - "./*" - ] - }, - }, - "include": [ - "**/*.ts", - "**/*.tsx", - "tsup.config.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/apps/server/tsup.config.ts b/apps/server/tsup.config.ts deleted file mode 100644 index 11a619b2..00000000 --- a/apps/server/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig({ - clean: true, - dts: false, - entry: ["src/index.ts"], - format: ["cjs"], - minify: isProduction, - sourcemap: true, -}); diff --git a/package.json b/package.json index 4d2bcad9..ff5e8256 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "prettier:all": "prettier --ignore-path .eslintignore \"**/*.{js,jsx,ts,tsx,md}\"", "--shortcuts to run commands in workspaces--": "", "frontend": "yarn workspace frontend", - "server": "yarn workspace server", "admin": "yarn workspace admin", "backend": "yarn workspace backend", "prisma": "yarn workspace @celluloid/prisma", diff --git a/packages/passport/src/index.ts b/packages/passport/src/index.ts index 45101d08..928d38d2 100644 --- a/packages/passport/src/index.ts +++ b/packages/passport/src/index.ts @@ -2,5 +2,6 @@ export * from "./errors"; export { createSession } from "./session"; import passport from './passport'; +export * from "./passport"; export { passport }; diff --git a/packages/passport/src/passport.ts b/packages/passport/src/passport.ts index 97ff6253..32209d47 100644 --- a/packages/passport/src/passport.ts +++ b/packages/passport/src/passport.ts @@ -46,27 +46,25 @@ passport.use( ); -const loginStrategy = new LocalStrategy( - { usernameField: "login" }, - async (login, password, done) => { +const loginStrategy = new LocalStrategy(async (login, password, done) => { - const user = await prisma.user.findFirst({ - where: { - OR: [{ email: login }, { username: login, }] - } - }); - - if (!user) { - return Promise.resolve(done(new InvalidUserError("User not found"))); - } - if (!bcrypt.compareSync(password, user.password)) { - return Promise.resolve(done(new InvalidUserError(`Login failed for user ${user.username}: incorrect password`))); + const user = await prisma.user.findFirst({ + where: { + OR: [{ email: login }, { username: login, }] } - if (!user.confirmed && user.role !== UserRole.Student) { - return Promise.resolve(done(new UserNotConfirmed(`Login failed: ${user.username} is not confirmed`))); - } - return Promise.resolve(done(null, user)); + }); + + if (!user) { + return done(new InvalidUserError("User not found")); + } + if (!bcrypt.compareSync(password, user.password)) { + return done(new InvalidUserError(`Login failed for user ${user.username}: incorrect password`)); } + if (!user.confirmed && user.role !== UserRole.Student) { + return done(new UserNotConfirmed(`Login failed: ${user.username} is not confirmed`)); + } + return done(null, user); +} ); passport.use(SigninStrategy.LOGIN, loginStrategy); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index c79991fb..447fe1c4 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -24,6 +24,7 @@ "dev": "tsup --watch" }, "dependencies": { + "@celluloid/passport": "*", "@celluloid/prisma": "*", "@trpc/server": "^10.40.0", "bcryptjs": "^2.4.3", @@ -33,6 +34,9 @@ "express-session": "^1.17.3", "js2xmlparser": "^5.0.0", "lodash": "^4.17.21", + "mjml": "^4.14.1", + "nodemailer": "^6.9.7", + "nodemailer-smtp-transport": "^2.7.4", "papaparse": "^5.4.1", "trpc-openapi": "^1.2.0", "uuid": "^9.0.1", @@ -41,6 +45,7 @@ "devDependencies": { "@celluloid/config": "*", "@types/express-session": "^1.17.8", + "@types/mjml": "^4.7.3", "@types/uuid": "^9.0.4", "tsup": "^7.2.0" } diff --git a/packages/trpc/src/mailer/sendMail.ts b/packages/trpc/src/mailer/sendMail.ts new file mode 100644 index 00000000..2dea4e91 --- /dev/null +++ b/packages/trpc/src/mailer/sendMail.ts @@ -0,0 +1,93 @@ +import mjml2html from 'mjml'; +import * as nodemailer from "nodemailer"; + +import getTransport from "./transport"; + +const EMAIL_FROM = process.env.EMAIL_FROM || "no-reply@celluloid.huma-num.fr"; + +const isDev = process.env.NODE_ENV !== "production"; + + +export async function sendMail( + to: string, subject: string, html: string) { + const transport = await getTransport(); + const mailOptions = { + from: `Celluloid <${EMAIL_FROM}>`, to, subject, html + }; + + const info = await transport.sendMail(mailOptions); + + if (isDev) { + const url = nodemailer.getTestMessageUrl(info); + if (url) { + // Hex codes here equivalent to chalk.blue.underline + console.log( + `Development email preview: \x1B[34m\x1B[4m${url}\x1B[24m\x1B[39m` + ); + } + } + +} + + +export async function sendPasswordReset({ username, email, code }: { username: string, email: string, code: string }) { + const subject = `${username} : réinitialisation de votre mot de passe Celluloid`; + + const mjmlTemplate = ` + + + + + Bonjour ${username}, + Nous avons reçu une demande de réinitialisation de mot de passe pour l'adresse email ${email} + Voici votre code de confirmation : ${code} + Ce code sera valable pendant 1 heure. + Veuillez le saisir dans le formulaire prévu à cet effet. + Si vous n'êtes pas à l'origine de cette demande, veuillez simplement ignorer ce mail. + Cordialement, + L'équipe Celluloid + + + + + `; + + const { html } = mjml2html(mjmlTemplate); + await sendMail(email, subject, html) +} + + + +export async function sendConfirmationCode({ username, email, code }: { username: string, email: string, code: string }) { + const subject = `${username} : réinitialisation de votre mot de passe Celluloid`; + + const mjmlTemplate = ` + + + + + + Bonjour ${username}, + + + Voici votre code de confirmation : ${code} + + + Ce code est valable pendant 1 heure. + + + Veuillez le saisir dans le formulaire prévu à cet effet. + + + L'équipe Celluloid vous souhaite la bienvenue ! + + + + + + `; + + const { html } = mjml2html(mjmlTemplate); + await sendMail(email, subject, html) +} + diff --git a/packages/trpc/src/mailer/transport.ts b/packages/trpc/src/mailer/transport.ts new file mode 100644 index 00000000..45a56c12 --- /dev/null +++ b/packages/trpc/src/mailer/transport.ts @@ -0,0 +1,72 @@ +import { promises as fsp } from "fs"; +import * as nodemailer from "nodemailer"; + +const { readFile, writeFile } = fsp; + +const isTest = process.env.NODE_ENV === "test"; +const isDev = process.env.NODE_ENV !== "production"; + +let transporterPromise: Promise; +const etherealFilename = `${process.cwd()}/.ethereal`; + +let logged = false; + +export default function getTransport(): Promise { + if (!transporterPromise) { + transporterPromise = (async () => { + if (isTest) { + return nodemailer.createTransport({ + jsonTransport: true, + }); + } else if (isDev) { + let account; + try { + const testAccountJson = await readFile(etherealFilename, "utf8"); + account = JSON.parse(testAccountJson); + } catch (e: any) { + account = await nodemailer.createTestAccount(); + await writeFile(etherealFilename, JSON.stringify(account)); + } + if (!logged) { + logged = true; + console.log(); + console.log(); + console.log( + // Escapes equivalent to chalk.bold + "\x1B[1m" + + " ✉️ Emails in development are sent via ethereal.email; your credentials follow:" + + "\x1B[22m" + ); + console.log(" Site: https://ethereal.email/login"); + console.log(` Username: ${account.user}`); + console.log(` Password: ${account.pass}`); + console.log(); + console.log(); + } + return nodemailer.createTransport({ + host: "smtp.ethereal.email", + port: 587, + secure: false, + auth: { + user: account.user, + pass: account.pass, + }, + }); + } else { + if (!process.env.SMTP_HOST) { + throw new Error("Misconfiguration: no SMTP_HOST"); + } + if (!process.env.SMTP_PORT) { + throw new Error("Misconfiguration: no SMTP_PORT"); + } + return nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || "465", 10), + secure: process.env.SMTP_SECURE === 'true', + }); + } + })(); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return transporterPromise!; +} diff --git a/packages/trpc/src/routers/user.ts b/packages/trpc/src/routers/user.ts index db57cee6..08db383f 100644 --- a/packages/trpc/src/routers/user.ts +++ b/packages/trpc/src/routers/user.ts @@ -1,30 +1,381 @@ -import { prisma } from "@celluloid/prisma" +import { passport, SigninStrategy } from "@celluloid/passport"; +import { Prisma, prisma, UserRole } from "@celluloid/prisma" import { TRPCError } from "@trpc/server"; import { z } from 'zod'; +import { sendConfirmationCode, sendPasswordReset } from "../mailer/sendMail"; import { protectedProcedure, publicProcedure, router } from '../trpc'; +import { compareCodes, generateOtp, hashPassword } from "../utils/forgot"; + +export const defaultUserSelect = Prisma.validator()({ + id: true, + username: true, + role: true, + initial: true, + color: true, +}); export const UserSchema = z.object({ - id: z.string(), - username: z.string(), - role: z.string(), - initial: z.string(), - color: z.string(), + id: z.string({ description: 'The unique identifier for the user' }), + username: z.string({ description: 'The username for the user' }), + role: z.nativeEnum(UserRole, { description: 'The role assigned to the user, either Admin or User' }).nullable(), + initial: z.string({ description: 'The initial letter or string for user representation' }), + color: z.string({ description: 'The color code associated with the user' }) }); export const userRouter = router({ - login: publicProcedure.input( + login: publicProcedure + .meta({ + openapi: { + method: 'POST', + path: '/login', + description: 'This endpoint allows a user to login.' + } + }) + .input( + z.object({ + username: z.string({ description: 'The username of the user' }), + password: z.string({ description: 'The password for the user' }) + }), + ).output(UserSchema.nullable()) + .mutation(async ({ ctx, input }) => { + + ctx.req.body = input; + + await new Promise((resolve, reject) => { + passport.authenticate(SigninStrategy.LOGIN, { + failWithError: true + })(ctx.req, ctx.res, (err: Error, user: Express.User) => { + if (err) return reject(err); + resolve(user); + }) + }).catch(err => { + console.log(err.name); + + if (err?.name === 'AuthenticationError') { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Incorrect username or password.' + }) + } else if (err?.name === "UserNotConfirmed") { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'UserNotConfirmed' + }) + } + + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: err + }) + }) + + const user = await prisma.user.findFirst({ + select: defaultUserSelect, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } + }); + + return user; + }), + forgot: publicProcedure.input( + z.object({ + email: z.string() + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true }, + where: { + OR: [{ email: input.email }, { username: input.email, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + if (!user.email) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `User account doesn't have email address`, + }); + } + + const otp = generateOtp(); + await prisma.user.update({ + where: { id: user.id }, + data: { + code: otp, + codeGeneratedAt: new Date() + } + }) + await sendPasswordReset({ email: user.email, username: user.username, code: otp }) + return { status: true } + }), + + recover: publicProcedure.input( + z.object({ + username: z.string(), + code: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + + if (!compareCodes(input.code, user.code || "")) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Failed to recover account, code invalid` + }); + } + + const newUser = await prisma.user.update({ + where: { id: user.id }, + data: { + password: hashPassword(input.password), + code: null, + codeGeneratedAt: null, + confirmed: true + } + }) + + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + + return { status: true } + + }), + + register: publicProcedure.input( + z.object({ + username: z.string(), + email: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + + + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.email }, { username: input.username, }] + } + }); + + if (user) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `ACCOUNT_EXISTS`, + }); + } + + const code = generateOtp(); + const newUser = await prisma.user.create({ + select: { + ...defaultUserSelect, email: true + }, + data: { + username: input.username, + email: input.email, + password: hashPassword(input.password), + code: code, + codeGeneratedAt: new Date(), + confirmed: false, + role: "Teacher" + } + }) + + await sendConfirmationCode({ username: newUser.username, email: input.email, code: code }); + + return newUser + + }), + registerAsStudent: publicProcedure.input( + z.object({ + username: z.string(), + shareCode: z.string(), + password: z.string().min(8) + }), + ).mutation(async ({ ctx, input }) => { + + + const project = await prisma.project.findUnique({ + where: { shareCode: input.shareCode } + + }); + + if (!project) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `CODE_NOT_FOUND`, + }); + } + + + const user = await prisma.user.findFirst({ + select: defaultUserSelect, + where: { + username: input.username + } + }); + + if (user) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: `ACCOUNT_EXISTS`, + }); + } + + + const newUser = await prisma.user.create({ + select: { + ...defaultUserSelect + }, + data: { + username: input.username, + password: hashPassword(input.password), + confirmed: true, + role: "Student" + } + }) + + await prisma.project.update({ + where: { id: project.id }, + data: { + members: { + create: [{ + userId: newUser.id, + }], + } + } + }) + + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + + return { projectId: project.id } + + }), + join: protectedProcedure.input( + z.object({ + shareCode: z.string(), + }), + ).mutation(async ({ ctx, input }) => { + + const project = await prisma.project.findUnique({ + where: { shareCode: input.shareCode } + + }); + + if (!project) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `CODE_NOT_FOUND`, + }); + } + + await prisma.project.update({ + where: { id: project.id }, + data: { + members: { + create: [{ + userId: ctx.user?.id + }], + } + } + }) + return { projectId: project.id } + }), + + askEmailConfirm: publicProcedure.input( + z.object({ + email: z.string() + }), + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true }, + where: { + OR: [{ email: input.email }, { username: input.email, }] + } + }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + if (!user.email) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `User account doesn't have email address`, + }); + } + + const otp = generateOtp(); + await prisma.user.update({ + where: { id: user.id }, + data: { + code: otp, + codeGeneratedAt: new Date() + } + }) + await sendConfirmationCode({ email: user.email, username: user.username, code: otp }) + return { status: true } + }), + + confirm: publicProcedure.input( z.object({ username: z.string(), - password: z.string() + code: z.string(), }), - ).mutation(async ({ input }) => { - ; - throw new TRPCError({ - code: 'UNAUTHORIZED', + ).mutation(async ({ ctx, input }) => { + const user = await prisma.user.findFirst({ + select: { ...defaultUserSelect, email: true, code: true }, + where: { + OR: [{ email: input.username }, { username: input.username, }] + } }); + + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Email or username not found`, + }); + } + + if (!compareCodes(input.code, user.code || "")) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Failed to confirm account, code invalid` + }); + } + + const newUser = await prisma.user.update({ + where: { id: user.id }, + data: { + code: null, + codeGeneratedAt: null, + confirmed: true + } + }) + await new Promise((resolve) => ctx.req.login(newUser, () => resolve(null))); + return { status: true } }), list: protectedProcedure.query(async () => { @@ -41,6 +392,7 @@ export const userRouter = router({ if (ctx.user) { // Retrieve the user with the given ID const user = await prisma.user.findUnique({ + select: { ...defaultUserSelect, email: true }, where: { id: ctx.user.id } }); return user; @@ -54,7 +406,7 @@ export const userRouter = router({ ).query(async (opts) => { const { input } = opts; // Retrieve the user with the given ID - const user = await prisma.user.findUnique({ where: { id: input.id } }); + const user = await prisma.user.findUnique({ select: defaultUserSelect, where: { id: input.id } }); return user; }), projects: protectedProcedure @@ -86,13 +438,7 @@ export const userRouter = router({ }, include: { user: { - select: { - id: true, - username: true, - role: true, - initial: true, - color: true - } + select: defaultUserSelect }, members: true, playlist: { diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index a631a35c..b15067a2 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -3,7 +3,7 @@ import "express-session" import { User, UserRole } from '@celluloid/prisma'; import { initTRPC, TRPCError } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; -import { Request } from "express"; +import { type Request, type Response } from 'express'; import { Session } from "express-session"; import { OpenApiMeta } from 'trpc-openapi'; import { v4 as uuid } from 'uuid'; @@ -13,6 +13,8 @@ export type Context = { requestId: string; requirePermissions: (roles: UserRole[]) => boolean; logout: () => Promise; + req: Request; + res: Response; }; export const createRPCContext = async ({ @@ -47,9 +49,13 @@ export const createRPCContext = async ({ }); }) } - return { user, requirePermissions, logout, requestId }; + return { + user, requirePermissions, logout, requestId, req, + res, + }; }; + const t = initTRPC.context().meta().create({ // transformer: SuperJSON }); diff --git a/packages/trpc/src/utils/forgot.ts b/packages/trpc/src/utils/forgot.ts new file mode 100644 index 00000000..a7d0c44f --- /dev/null +++ b/packages/trpc/src/utils/forgot.ts @@ -0,0 +1,19 @@ + +import bcrypt from 'bcryptjs'; + +export function hashPassword(password: string) { + const salt = bcrypt.genSaltSync(); + return bcrypt.hashSync(password, salt); +} + + +export function generateOtp() { + // Generate a 4-digit OTP + const otp: string = Math.floor(1000 + Math.random() * 9000).toString(); + return otp; +} + + +export function compareCodes(expected: string, actual: string) { + return expected.replace(/\s/g, "") === actual.replace(/\s/g, ""); +} diff --git a/packages/types/src/AnnotationTypes.ts b/packages/types/src/AnnotationTypes.ts deleted file mode 100644 index d61230fa..00000000 --- a/packages/types/src/AnnotationTypes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CommentRecord } from './CommentTypes'; -import { UserRecord } from './UserTypes'; - -export interface AnnotationData { - text: string; - startTime: number; - stopTime: number; - pause: boolean; -} - -export interface AnnotationRecord extends AnnotationData { - projectId: string; - userId: string; - id: string; - user: UserRecord; - comments: CommentRecord[]; -} \ No newline at end of file diff --git a/packages/types/src/CommentTypes.ts b/packages/types/src/CommentTypes.ts deleted file mode 100644 index c53bccb0..00000000 --- a/packages/types/src/CommentTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UserRecord } from './UserTypes'; - -export interface CommentRecord { - id: string; - annotationId: string; - userId: string; - text: string; - createdAt: Date; - user: UserRecord; -} \ No newline at end of file diff --git a/packages/types/src/ProjectTypes.ts b/packages/types/src/ProjectTypes.ts deleted file mode 100644 index e25b2c0a..00000000 --- a/packages/types/src/ProjectTypes.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { TagData } from './TagTypes'; -import { UserRecord } from './UserTypes'; - -export interface ProjectCreateData { - videoId: string; - title: string; - host: string; - description?: string; - objective: string; - assignments: Array; - tags: TagData[]; - levelStart: number; - levelEnd: number; - public: boolean; - collaborative: boolean; -} - -export interface ProjectUpdateData { - title: string; - description?: string; - objective: string; - assigments: Array; - tags: TagData[]; - levelStart: number; - leverEnd: number; - public: boolean; - collaborative: boolean; -} - -export interface ProjectRecord extends ProjectCreateData { - id: string; - userId: string; - publishedAt: Date; - shared: boolean; - shareName: string; - sharePassword: string; - shareExpiresAt: string; -} - -export interface ProjectGraphRecord extends ProjectRecord { - user: UserRecord; - members: UserRecord[]; -} - -export interface ProjectShareData { - sharePassword: string; - // shareExpiresAt: Date; - // shareMaxUsers: number; -} diff --git a/packages/types/src/TagTypes.ts b/packages/types/src/TagTypes.ts deleted file mode 100644 index ce2e89b5..00000000 --- a/packages/types/src/TagTypes.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface TagData { - id: string; - name: string; - featured: boolean; -} \ No newline at end of file diff --git a/packages/types/src/UnfurlTypes.ts b/packages/types/src/UnfurlTypes.ts deleted file mode 100644 index 977bf3a5..00000000 --- a/packages/types/src/UnfurlTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface UnfurlData { - website: string; - faviconUrl?: string; - title: string; - description?: string; - imageUrl?: string; -} \ No newline at end of file diff --git a/packages/types/src/UserTypes.ts b/packages/types/src/UserTypes.ts deleted file mode 100644 index 6bec594d..00000000 --- a/packages/types/src/UserTypes.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { TagData } from './TagTypes'; - -export interface SigninErrors { - login?: string; - password?: string; - email?: string; - username?: string; - confirmPassword?: string; - code?: string; - server?: string; - shareCode?: string; -} - -export interface SigninResult { - success: boolean; - errors: SigninErrors; -} - -export interface TeacherData { - email: string; - username: string; - subjects?: TagData[]; -} - -export interface TeacherRecord extends TeacherData { - id: string; -} - -export interface TeacherSignupData extends TeacherData { - password: string; -} - -export interface TeacherConfirmData { - login: string; - code: string; -} - -export interface Credentials { - login: string; - password: string; -} - -export interface StudentData { - username: string; -} - -export interface StudentRecord extends StudentData { - id: string; -} - -export interface StudentSignupData { - username: string; - password: string; - shareCode: string; -} - -export interface TeacherConfirmResetPasswordData extends TeacherConfirmData { - password: string; -} - -export interface ConfirmSignupErrors { - email: string; - code: string; -} - -export interface ConfirmSignupValidation { - success: boolean; - errors?: ConfirmSignupErrors; -} - -type UserRole - = 'Admin' - | 'Teacher' - | 'Student' - ; - -export interface UserRecord { - id: string; - username: string; - role: UserRole; - email?: string; -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fdf4de9b..bee8dd37 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,8 +1,2 @@ -export * from "./AnnotationTypes"; -export * from "./CommentTypes"; export * from "./PeerTubeVideo"; export * from "./PeerTubeVideoMetadata"; -export * from "./ProjectTypes"; -export * from "./TagTypes"; -export * from "./UnfurlTypes"; -export * from "./UserTypes"; diff --git a/packages/validators/package.json b/packages/validators/package.json deleted file mode 100644 index 313c15c3..00000000 --- a/packages/validators/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@celluloid/validators", - "main": "dist/index.js", - "version": "0.1.0", - "description": "Validation library for Celluloid client and server types", - "repository": "http://github.com/celluloid-edu/celluloid", - "author": "Younes Benaomar ", - "license": "MIT", - "scripts": { - "build": "tsup", - "dev": "tsup --watch --silent" - }, - "devDependencies": { - "@celluloid/types": "*", - "@types/validator": "13.7.10", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "validator": "13.7.0" - } -} diff --git a/packages/validators/src/UserValidator.ts b/packages/validators/src/UserValidator.ts deleted file mode 100644 index df4de555..00000000 --- a/packages/validators/src/UserValidator.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - Credentials, - SigninResult, - StudentSignupData, - TeacherConfirmData, - TeacherConfirmResetPasswordData, - TeacherSignupData, -} from '@celluloid/types'; -import validator from 'validator'; - -export function validateSignup(payload: TeacherSignupData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.username !== 'string' || - payload.username.trim().length === 0) { - result.success = false; - result.errors.username = `UsernameMissing`; - } - - if (!payload || typeof payload.email !== 'string' || - !validator.isEmail(payload.email)) { - result.success = false; - result.errors.email = 'InvalidEmailFormat'; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length < 8) { - result.success = false; - result.errors.password = 'InvalidPasswordFormat'; - } - return result; -} - -export function validateConfirmationCode(code: string): boolean { - const codeRegExp = /^[0-9]{6}$/; - const trimmedCode = code.replace(/\s/g, ''); - return codeRegExp.test(trimmedCode); -} - -export function validateConfirmResetPassword( - payload: TeacherConfirmResetPasswordData -) { - const result = { - success: true, - errors: {} - } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.login = 'MissingLogin'; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length < 8) { - result.success = false; - result.errors.password = 'InvalidPasswordFormat'; - } - - if (!payload || typeof payload.code !== 'string' || - !validateConfirmationCode(payload.code)) { - result.success = false; - result.errors.code = `InvalidCodeFormat`; - } - return result; -} - -export function validateConfirmSignup(payload: TeacherConfirmData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.email = 'MissingLogin'; - } - - if (!payload || typeof payload.code !== 'string' || - !validateConfirmationCode(payload.code)) { - result.success = false; - result.errors.code = `InvalidCodeFormat`; - } - - return result; -} - -export function validateLogin(payload: Credentials) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.login !== 'string' || - payload.login.trim().length === 0) { - result.success = false; - result.errors.login = `MissingLogin`; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length === 0) { - result.success = false; - result.errors.password = 'MissingPassword'; - } - - return result; -} - -export function validateStudentSignup(payload: StudentSignupData) { - const result = { success: true, errors: {} } as SigninResult; - - if (!payload || typeof payload.shareCode !== 'string' || - payload.shareCode.trim().length === 0) { - result.success = false; - result.errors.shareCode = `MissingShareCode`; - } - - if (!payload || typeof payload.username !== 'string' || - payload.username.trim().length === 0) { - result.success = false; - result.errors.username = `MissingUsername`; - } - - if (!payload || typeof payload.password !== 'string' || - payload.password.trim().length === 0) { - result.success = false; - result.errors.password = 'MissingPassword'; - } - - return result; -} diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts deleted file mode 100644 index 80e5c679..00000000 --- a/packages/validators/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './UserValidator'; \ No newline at end of file diff --git a/packages/validators/tsconfig.json b/packages/validators/tsconfig.json deleted file mode 100644 index 847685fc..00000000 --- a/packages/validators/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@celluloid/config/tsconfig/node16.json", - "include": [ - "**/*.ts", - "**/*.tsx", - "tsup.config.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/packages/validators/tsup.config.ts b/packages/validators/tsup.config.ts deleted file mode 100644 index db41265e..00000000 --- a/packages/validators/tsup.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "tsup"; - -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig({ - clean: true, - dts: true, - entry: ["src/index.ts"], - format: ["esm", "cjs"], - minify: isProduction, - sourcemap: true, -}); diff --git a/yarn.lock b/yarn.lock index af7b4b9f..b9b80b6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1770,7 +1770,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.23.1": +"@babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.23.1": version: 7.23.2 resolution: "@babel/runtime@npm:7.23.2" dependencies: @@ -1918,9 +1918,11 @@ __metadata: resolution: "@celluloid/trpc@workspace:packages/trpc" dependencies: "@celluloid/config": "*" + "@celluloid/passport": "*" "@celluloid/prisma": "*" "@trpc/server": ^10.40.0 "@types/express-session": ^1.17.8 + "@types/mjml": ^4.7.3 "@types/uuid": ^9.0.4 bcryptjs: ^2.4.3 change-case: ^4.1.2 @@ -1929,6 +1931,9 @@ __metadata: express-session: ^1.17.3 js2xmlparser: ^5.0.0 lodash: ^4.17.21 + mjml: ^4.14.1 + nodemailer: ^6.9.7 + nodemailer-smtp-transport: ^2.7.4 papaparse: ^5.4.1 trpc-openapi: ^1.2.0 tsup: ^7.2.0 @@ -3560,6 +3565,13 @@ __metadata: languageName: node linkType: hard +"@one-ini/wasm@npm:0.1.1": + version: 0.1.1 + resolution: "@one-ini/wasm@npm:0.1.1" + checksum: 11de17108eae57c797e552e36b259398aede999b4a689d78be6459652edc37f3428472410590a9d328011a8751b771063a5648dd5c4205631c55d1d58e313156 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3709,26 +3721,6 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:^1.8.6": - version: 1.9.6 - resolution: "@reduxjs/toolkit@npm:1.9.6" - dependencies: - immer: ^9.0.21 - redux: ^4.2.1 - redux-thunk: ^2.4.2 - reselect: ^4.1.8 - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - checksum: 61d445f7e084c79f9601f61fcfc4eb65152b850b2a4330239d982297605bd870e63dc1e0211deb3822392cd3bc0c88ca0cdb236a9711a4311dfb199c607b6ac5 - languageName: node - linkType: hard - "@remirror/core-constants@npm:^2.0.2": version: 2.0.2 resolution: "@remirror/core-constants@npm:2.0.2" @@ -5507,6 +5499,22 @@ __metadata: languageName: node linkType: hard +"@types/mjml-core@npm:*": + version: 4.7.3 + resolution: "@types/mjml-core@npm:4.7.3" + checksum: c6d002cc599806a9603e48f3b848a48c54a01a9b8cbc147b1cf01355e80a0ed1be720928e9e290c0dc876ba826fb1f84e7620d8e6edf53b48ce27fa06a73e6e2 + languageName: node + linkType: hard + +"@types/mjml@npm:^4.7.3": + version: 4.7.3 + resolution: "@types/mjml@npm:4.7.3" + dependencies: + "@types/mjml-core": "*" + checksum: c7d31acaea2495cd58bb3ce89f0cb4e52e938be6cfb4262f5be4a3887f24eed50674324d5a25a1797e48c1c7ee295dc2e21c73d45f47b02f1d0a6bcfc14009e4 + languageName: node + linkType: hard + "@types/moment-duration-format@npm:^2.2.0": version: 2.2.4 resolution: "@types/moment-duration-format@npm:2.2.4" @@ -6789,6 +6797,13 @@ __metadata: languageName: node linkType: hard +"ansi-colors@npm:^4.1.1": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: a9c2ec842038a1fabc7db9ece7d3177e2fe1c5dc6f0c51ecfbf5f39911427b89c00b5dc6b8bd95f82a26e9b16aaae2e83d45f060e98070ce4d1333038edceb0e + languageName: node + linkType: hard + "ansi-escapes@npm:^3.0.0, ansi-escapes@npm:^3.2.0": version: 3.2.0 resolution: "ansi-escapes@npm:3.2.0" @@ -8020,6 +8035,16 @@ __metadata: languageName: node linkType: hard +"camel-case@npm:^3.0.0": + version: 3.0.0 + resolution: "camel-case@npm:3.0.0" + dependencies: + no-case: ^2.2.0 + upper-case: ^1.1.1 + checksum: 4190ed6ab8acf4f3f6e1a78ad4d0f3f15ce717b6bfa1b5686d58e4bcd29960f6e312dd746b5fa259c6d452f1413caef25aee2e10c9b9a580ac83e516533a961a + languageName: node + linkType: hard + "camel-case@npm:^4.1.2": version: 4.1.2 resolution: "camel-case@npm:4.1.2" @@ -8313,7 +8338,7 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:^1.0.0-rc.2, cheerio@npm:^1.0.0-rc.3": +"cheerio@npm:1.0.0-rc.12, cheerio@npm:^1.0.0-rc.12, cheerio@npm:^1.0.0-rc.2, cheerio@npm:^1.0.0-rc.3": version: 1.0.0-rc.12 resolution: "cheerio@npm:1.0.0-rc.12" dependencies: @@ -8328,7 +8353,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": +"chokidar@npm:^3.0.0, chokidar@npm:^3.4.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -8389,6 +8414,15 @@ __metadata: languageName: node linkType: hard +"clean-css@npm:^4.2.1": + version: 4.2.4 + resolution: "clean-css@npm:4.2.4" + dependencies: + source-map: ~0.6.0 + checksum: 045ff6fcf4b5c76a084b24e1633e0c78a13b24080338fc8544565a9751559aa32ff4ee5886d9e52c18a644a6ff119bd8e37bc58e574377c05382a1fb7dbe39f8 + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.2 resolution: "clean-css@npm:5.3.2" @@ -8768,7 +8802,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.0": +"commander@npm:^6.1.0, commander@npm:^6.2.0": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: d7090410c0de6bc5c67d3ca41c41760d6d268f3c799e530aafb73b7437d1826bbf0d2a3edac33f8b57cc9887b4a986dce307fa5557e109be40eadb7c43b21742 @@ -8899,6 +8933,16 @@ __metadata: languageName: node linkType: hard +"config-chain@npm:^1.1.13": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: ^1.3.4 + proto-list: ~1.2.1 + checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab + languageName: node + linkType: hard + "configstore@npm:^5.0.1": version: 5.0.1 resolution: "configstore@npm:5.0.1" @@ -8936,29 +8980,6 @@ __metadata: languageName: node linkType: hard -"connected-react-router@npm:6.9.3": - version: 6.9.3 - resolution: "connected-react-router@npm:6.9.3" - dependencies: - immutable: ^3.8.1 || ^4.0.0 - lodash.isequalwith: ^4.4.0 - prop-types: ^15.7.2 - seamless-immutable: ^7.1.3 - peerDependencies: - history: ^4.7.2 - react: ^16.4.0 || ^17.0.0 - react-redux: ^6.0.0 || ^7.1.0 - react-router: ^4.3.1 || ^5.0.0 - redux: ^3.6.0 || ^4.0.0 - dependenciesMeta: - immutable: - optional: true - seamless-immutable: - optional: true - checksum: 047a11c2f3c9993087f3cd467789445781320c61f3184e1016e7b05862a275006004867231cc7396c7f213afd2fbce7e8ac0df39ba2cda7502d72a140657f9e7 - languageName: node - linkType: hard - "consola@npm:^3.2.3": version: 3.2.3 resolution: "consola@npm:3.2.3" @@ -10046,6 +10067,13 @@ __metadata: languageName: node linkType: hard +"detect-node@npm:2.0.4": + version: 2.0.4 + resolution: "detect-node@npm:2.0.4" + checksum: c06ae40fefbad8cb8cbb6ca819c93568b2a809e747bfc9c71f3524b027f5e988163b0ac0517fd65288b375360b30bc4822172eb05d211f99003d73cf8ec22911 + languageName: node + linkType: hard + "detect-node@npm:^2.0.4": version: 2.1.0 resolution: "detect-node@npm:2.1.0" @@ -10255,6 +10283,15 @@ __metadata: languageName: node linkType: hard +"domhandler@npm:^3.3.0": + version: 3.3.0 + resolution: "domhandler@npm:3.3.0" + dependencies: + domelementtype: ^2.0.1 + checksum: 850e5e9fee7834ab4314811e18bc1f4294d7eafbf6a79ad03cbe50cf964108935c97257ac248944d72a9312b4a18dfa8323e857d23278964dc83b1f124467fa3 + languageName: node + linkType: hard + "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": version: 4.3.1 resolution: "domhandler@npm:4.3.1" @@ -10283,7 +10320,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^2.5.2, domutils@npm:^2.8.0": +"domutils@npm:^2.4.2, domutils@npm:^2.5.2, domutils@npm:^2.8.0": version: 2.8.0 resolution: "domutils@npm:2.8.0" dependencies: @@ -10436,6 +10473,20 @@ __metadata: languageName: node linkType: hard +"editorconfig@npm:^1.0.3": + version: 1.0.4 + resolution: "editorconfig@npm:1.0.4" + dependencies: + "@one-ini/wasm": 0.1.1 + commander: ^10.0.0 + minimatch: 9.0.1 + semver: ^7.5.3 + bin: + editorconfig: bin/editorconfig + checksum: 09904f19381b3ddf132cea0762971aba887236f387be3540909e96b8eb9337e1793834e10f06890cd8e8e7bb1ba80cb13e7d50a863f227806c9ca74def4165fb + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -12305,7 +12356,6 @@ __metadata: "@mui/lab": ^5.0.0-alpha.148 "@mui/material": ^5.14.13 "@mui/styles": ^5.14.13 - "@reduxjs/toolkit": ^1.8.6 "@tanstack/react-query": ^4.36.1 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^13.4.0 @@ -12343,7 +12393,6 @@ __metadata: autosuggest-highlight: ^3.3.4 axios: ^1.3.4 change-case: ^4.1.2 - connected-react-router: 6.9.3 copy-to-clipboard: ^3.3.3 dayjs: ^1.11.10 enzyme: ^3.3.0 @@ -12365,7 +12414,6 @@ __metadata: notistack: ^3.0.1 passport: ^0.6.0 passport-local: ^1.0.0 - prop-types: ^15.6.2 query-string: ^6.1.0 ramda: ^0.28.0 randomcolor: ^0.5.3 @@ -12377,16 +12425,12 @@ __metadata: react-error-boundary: ^4.0.11 react-full-screen: ^0.2.2 react-i18next: ^13.2.2 - react-redux: ^8.0.4 react-router: ^6.17.0 react-router-dom: ^6.17.0 react-scripts: 5.0.1 react-transition-group: ^2.3.1 react-use-event: ^1.1.1 recoil: ^0.7.7 - redux: ^4.0.0 - redux-devtools-extension: ^2.13.5 - redux-thunk: ^2.3.0 rooks: ^7.4.1 serve: ^14.2.1 shiitake: ^3.0.2 @@ -12394,6 +12438,7 @@ __metadata: vite: ^4.4.11 vite-aliases: ^0.11.3 yup: ^1.3.2 + yup-locales: ^1.2.18 languageName: unknown linkType: soft @@ -12852,7 +12897,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0, glob@npm:^8.0.3": +"glob@npm:^8.0.0, glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -13416,6 +13461,23 @@ __metadata: languageName: node linkType: hard +"html-minifier@npm:^4.0.0": + version: 4.0.0 + resolution: "html-minifier@npm:4.0.0" + dependencies: + camel-case: ^3.0.0 + clean-css: ^4.2.1 + commander: ^2.19.0 + he: ^1.2.0 + param-case: ^2.1.1 + relateurl: ^0.2.7 + uglify-js: ^3.5.1 + bin: + html-minifier: ./cli.js + checksum: b426aee771d9da104c1c9554e3ebd3a4f483d2ce01f4dcc4156ba33a5959044acf6bea192d5ae63b290cdb92c30a9d07fd6924c65609aa82382ce411328f94ca + languageName: node + linkType: hard + "html-parse-stringify@npm:^3.0.1": version: 3.0.1 resolution: "html-parse-stringify@npm:3.0.1" @@ -13447,6 +13509,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^5.0.0": + version: 5.0.1 + resolution: "htmlparser2@npm:5.0.1" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^3.3.0 + domutils: ^2.4.2 + entities: ^2.0.0 + checksum: b67ac02e44629ec76b712fc06702451bea64e522cfcd7cc22fa85023b81b44cde5060662faa81d34f18c0fe5a43ced1cac73528d30a6df5ac5825a4d479c7ea5 + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -13790,14 +13864,14 @@ __metadata: languageName: node linkType: hard -"immer@npm:^9.0.21, immer@npm:^9.0.7": +"immer@npm:^9.0.7": version: 9.0.21 resolution: "immer@npm:9.0.21" checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 languageName: node linkType: hard -"immutable@npm:^3.8.1 || ^4.0.0, immutable@npm:^4.2.2": +"immutable@npm:^4.2.2": version: 4.3.4 resolution: "immutable@npm:4.3.4" checksum: de3edd964c394bab83432429d3fb0b4816b42f56050f2ca913ba520bd3068ec3e504230d0800332d3abc478616e8f55d3787424a90d0952e6aba864524f1afc3 @@ -15535,6 +15609,22 @@ __metadata: languageName: node linkType: hard +"js-beautify@npm:^1.6.14": + version: 1.14.9 + resolution: "js-beautify@npm:1.14.9" + dependencies: + config-chain: ^1.1.13 + editorconfig: ^1.0.3 + glob: ^8.1.0 + nopt: ^6.0.0 + bin: + css-beautify: js/bin/css-beautify.js + html-beautify: js/bin/html-beautify.js + js-beautify: js/bin/js-beautify.js + checksum: aea5af03d0e8d5bcdfc9f98d6c6ebdc17076c762123ae79557d271a921438e2c0c422bc56a955119d770bb0f01cb411003534d8ae8dc138eb7af4821f21f8352 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -15871,6 +15961,21 @@ __metadata: languageName: node linkType: hard +"juice@npm:^9.0.0": + version: 9.1.0 + resolution: "juice@npm:9.1.0" + dependencies: + cheerio: ^1.0.0-rc.12 + commander: ^6.1.0 + mensch: ^0.3.4 + slick: ^1.12.2 + web-resource-inliner: ^6.0.1 + bin: + juice: bin/juice + checksum: 95f20fa183baa17360d7f03f2699f7cbc3476fb2e3a2d1d81d28f2ce1e5cd61a634a05cad26cfe83174c730ecbde18d8db9bc244b915741833fa6ce1c61c6864 + languageName: node + linkType: hard + "jw-paginate@npm:^1.0.4": version: 1.0.4 resolution: "jw-paginate@npm:1.0.4" @@ -16350,13 +16455,6 @@ __metadata: languageName: node linkType: hard -"lodash.isequalwith@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.isequalwith@npm:4.4.0" - checksum: 428ba7a57c47ec05e2dd18c03a4b4c45dac524a46af7ce3f412594bfc7be6a5acaa51acf9ea113d0002598e9aafc6e19ee8d20bc28363145fcb4d21808c9039f - languageName: node - linkType: hard - "lodash.isfunction@npm:^3.0.9": version: 3.0.9 resolution: "lodash.isfunction@npm:3.0.9" @@ -16526,6 +16624,13 @@ __metadata: languageName: node linkType: hard +"lower-case@npm:^1.1.1": + version: 1.1.4 + resolution: "lower-case@npm:1.1.4" + checksum: 1ca9393b5eaef94a64e3f89e38b63d15bc7182a91171e6ad1550f51d710ec941540a065b274188f2e6b4576110cc2d11b50bc4bb7c603a040ddeb1db4ca95197 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -16829,6 +16934,13 @@ __metadata: languageName: node linkType: hard +"mensch@npm:^0.3.4": + version: 0.3.4 + resolution: "mensch@npm:0.3.4" + checksum: eabb25d595b9bb7c067b932ea9c96f0c8154a4bb6c454a4edecef9f5c87652e345a40128741ed95905699c5a16ad1f6c7efd5f6dfc06e18128d74b569a4fb893 + languageName: node + linkType: hard + "meow@npm:^10.1.3": version: 10.1.5 resolution: "meow@npm:10.1.5" @@ -16947,6 +17059,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^2.4.6": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 1497ba7b9f6960694268a557eae24b743fd2923da46ec392b042469f4b901721ba0adcf8b0d3c2677839d0e243b209d76e5edcbd09cfdeffa2dfb6bb4df4b862 + languageName: node + linkType: hard + "mime@npm:^3.0.0": version: 3.0.0 resolution: "mime@npm:3.0.0" @@ -17032,6 +17153,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -17168,6 +17298,408 @@ __metadata: languageName: node linkType: hard +"mjml-accordion@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-accordion@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 66212dcf89531da230115c786dec24194d8ec9a4c93bcc1cfdbac332be07678eee3b8479d46f155cb60bf13358edd5cd7e4d6538ad5f9a910cbee5bb6b450855 + languageName: node + linkType: hard + +"mjml-body@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-body@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 27388e15681bb25412a7123ae82559e6cb5586293aef3aa2cf57138bee401c1b53e84d8efacef2c9db4cb7bf8dc8cac741b7907ec11036f2b804178db511301c + languageName: node + linkType: hard + +"mjml-button@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-button@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 55fa3228476fbb17c51d63fbc9a18ce280c3246a69164bbd6d93f4670b3a9f93e985cece5958cc94ff0b60fbc199bd1382fd85d27aac0677517926ec8dd0ad6f + languageName: node + linkType: hard + +"mjml-carousel@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-carousel@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: db6d7847722ef1d4fd2b74aba04853156c729ba1a99e5565fbe5c32ed96733de1846fc41995505ec950de4953fa415586251c1e65f731725edd9d4b08b259e87 + languageName: node + linkType: hard + +"mjml-cli@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-cli@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + chokidar: ^3.0.0 + glob: ^7.1.1 + html-minifier: ^4.0.0 + js-beautify: ^1.6.14 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + yargs: ^16.1.0 + bin: + mjml-cli: bin/mjml + checksum: ed3a08c68b6c5261e173674d1f1276b2cd636f2edc8713234a071befe919f9f9aa22e254480516d4b8d49eef22989017ce4327418c1c03fe08b004b6d1f8d136 + languageName: node + linkType: hard + +"mjml-column@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-column@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: a8eb4f321b9015ba8be96d08019502ada557fe3ba55413abf71b39a9ce209d0a3550ba03714a91b03b065af8b64582f6b3703b249f77c12bc1b54a499ac10ee2 + languageName: node + linkType: hard + +"mjml-core@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-core@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + cheerio: 1.0.0-rc.12 + detect-node: ^2.0.4 + html-minifier: ^4.0.0 + js-beautify: ^1.6.14 + juice: ^9.0.0 + lodash: ^4.17.21 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + checksum: fe46769b1746b1da90ddd39c584a6c8f7db80e125e079ce83cfd8ab4888e5abfff2933f573993926b36721de194b261c28f078b9316c395b185fd4098298c025 + languageName: node + linkType: hard + +"mjml-divider@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-divider@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b44fab9de9751caf626ca6206b58c2a9ac7788c54c56d91cc892f77ed164a0fd2021422ef1019adb147a145873db499bb89f1518aa4326face1135acd8f61294 + languageName: node + linkType: hard + +"mjml-group@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-group@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 17cec7be9544ae32121cac12cf6dffd0a234bb2c14e2e72df230c52f1834f34fbd2df6ed15661491d0eee2c95dd7ad77e6048ca0ca012c9970d96bcfe8a4e77e + languageName: node + linkType: hard + +"mjml-head-attributes@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-attributes@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 93783e5ce4df95c745fee65cf2a4787eadbd548bb2d35f4c408d50cd4f81652061da4fcf54b4861db40bef115b60bb29f36faf6478033ad32e5e467415ec394a + languageName: node + linkType: hard + +"mjml-head-breakpoint@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-breakpoint@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b52ea526f9291e0919ec82a7cc89e1e4d5a22c78280bc039b97648a3b938778d3bc7ff77b658a8a5d247c80327d2677f1591a5638039edc0d7c6f86670a1aeec + languageName: node + linkType: hard + +"mjml-head-font@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-font@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 3787977d042634ed338eb5d1be8612494a55419f568187c40517d3c53d57a93d3efd13c82c89d4a5b5c6456082bee12b6f682ededbc24a071600c9986a88ee94 + languageName: node + linkType: hard + +"mjml-head-html-attributes@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-html-attributes@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: a076f05954e09d7d8d721dc6931a1ecfcfa59126d4c7859c6278404d8e036b83f8eb72fd4285f367324d170bde7df64385ddf093b9f47cf5115fffd85756a510 + languageName: node + linkType: hard + +"mjml-head-preview@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-preview@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 9d8301458a93794695a1c50a16dbdd7914c008f0a89ee87be9d83f494966fb0aa51434549a6f183a014e34bfdc23795607bc33a33a1a4225882c8d0208fa3898 + languageName: node + linkType: hard + +"mjml-head-style@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-style@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 2e96180bd72656c70507f21a37f8cf3c0dc41709052af42e1161d77551df762f62d863635c18dff6d092bab9bd8c8c631c0a09b3c6dc25575f0693ee6627b7ba + languageName: node + linkType: hard + +"mjml-head-title@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head-title@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 65dfe9cb5115a9cfe76851b9e5aabaaa30131e55a4346e9ec04bde3234897ffe1ab3e7bb37a695af44deffe4a869dee34668a3d87396ed50b923310fb9baebcd + languageName: node + linkType: hard + +"mjml-head@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-head@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: c83c930badb7ad0ee5771b13928d2c371aa9b70777393e32361fa356b534d1b282f5698e41dee8f947c687d28580e80b74bac2d3308970884e58152edc86bafd + languageName: node + linkType: hard + +"mjml-hero@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-hero@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 6c6ec8e5168709f09175d030c2a6fc7326f7a2e076cf09c0676e78bd941521e2c4295335bfdce8b5c31ea946a1925edcb780aced73b0dbfda40c07a463526c93 + languageName: node + linkType: hard + +"mjml-image@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-image@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 1ad0910300b115fcc42de6d642ce35b2a3593ac1a431498a2a2f3210733ff7c2e4bc33334abbd20f9854c77aa0f7c859928941fa6cb0bce190453f857e7c7f90 + languageName: node + linkType: hard + +"mjml-migrate@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-migrate@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + js-beautify: ^1.6.14 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-parser-xml: 4.14.1 + yargs: ^16.1.0 + bin: + migrate: lib/cli.js + checksum: 6710d100d79fd0b066cfd2fd0a5f7e6d7ccbf309a31039f162a22ff7b69c0540e550325560737270b205a3a3cd4562603e6bc4a44424ca973c44168741c3f388 + languageName: node + linkType: hard + +"mjml-navbar@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-navbar@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: b85ccb20a95575387b5ce65e317f9a25fe46c1d77bab506274d630950da6bcbec1034cf351887eb1aec10e6c0b8b926804fc20cbed99209de45d49ada736f969 + languageName: node + linkType: hard + +"mjml-parser-xml@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-parser-xml@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + detect-node: 2.0.4 + htmlparser2: ^8.0.1 + lodash: ^4.17.15 + checksum: 839225d2d8c5b7c8a948ebe2a49afa8aa8f4e3651810b40df95d6f39da56ea6d62e2c4e5c55f96eb60d191233c0d2c77be0ee9cc861ffa5c3da032be56e0d96c + languageName: node + linkType: hard + +"mjml-preset-core@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-preset-core@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + mjml-accordion: 4.14.1 + mjml-body: 4.14.1 + mjml-button: 4.14.1 + mjml-carousel: 4.14.1 + mjml-column: 4.14.1 + mjml-divider: 4.14.1 + mjml-group: 4.14.1 + mjml-head: 4.14.1 + mjml-head-attributes: 4.14.1 + mjml-head-breakpoint: 4.14.1 + mjml-head-font: 4.14.1 + mjml-head-html-attributes: 4.14.1 + mjml-head-preview: 4.14.1 + mjml-head-style: 4.14.1 + mjml-head-title: 4.14.1 + mjml-hero: 4.14.1 + mjml-image: 4.14.1 + mjml-navbar: 4.14.1 + mjml-raw: 4.14.1 + mjml-section: 4.14.1 + mjml-social: 4.14.1 + mjml-spacer: 4.14.1 + mjml-table: 4.14.1 + mjml-text: 4.14.1 + mjml-wrapper: 4.14.1 + checksum: 86852c543c138fcafecd461ccecd03c36b0ac573a644fe47a164b8f94465c33eee25c815e6cb17a85bd947bccd21ffb700023a22d1f39e5540ba9b663c96e7ce + languageName: node + linkType: hard + +"mjml-raw@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-raw@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 65721432a89653644ae7e451e5a09d5168e6a69900f73823b74803ac4f4ff148ee4654db916e770e9e7a4e51cb83222c95b15b0220f21d96eeb9eb1a8571be7d + languageName: node + linkType: hard + +"mjml-section@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-section@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: f4b2ba3fa916193635b273d482a23e6f2f2969d01b5517e62d505ef5b6260e404bd2df3252ebd5926c1d5dc79f33cac8ceab19c161cf8435c3a23148c0296a15 + languageName: node + linkType: hard + +"mjml-social@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-social@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 4d493dcb133beb6361cc5b6ff5799ef8456e39fd89f60d1c8ecc8767eb2fcfedf5f0a253dfafa543c6c3a32a798cb3e009b59e6588fdce5726b057435cf5d3a6 + languageName: node + linkType: hard + +"mjml-spacer@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-spacer@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 93bf08f18da4a6593ded0675d32d0b2599d8fa9b00a3f3c0d90803106611f09a48efff803f82e740e27c8e5e56a36a40c66c87045ca7090ca5685762f0fe9382 + languageName: node + linkType: hard + +"mjml-table@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-table@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: f7fc1f648a112b8bab5209e9a3200926b1c10b39acc90f691e6b2e6d75a642ebf2f8f603b72676bd3490c3afaad97d06f1a64503dd971695f431760436317b26 + languageName: node + linkType: hard + +"mjml-text@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-text@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + checksum: 16133c363813a4ec5bef06fbd34789a59d06206f78c43e43f1979bb326169b9f0809c4ddf651a05ae8ea4b295dcce6ad80d6c696b628832a5357d3bb532a2d5d + languageName: node + linkType: hard + +"mjml-validator@npm:4.13.0": + version: 4.13.0 + resolution: "mjml-validator@npm:4.13.0" + dependencies: + "@babel/runtime": ^7.14.6 + checksum: 40397cc664ee0e1ad884ddef30e2ab1cb3b14bb3fb1730e9ba8d7a786c25a260726b4bb70bae7094aa4177a369fd46bd2bf7f8e744f9cdecd0c3ceb8881b075e + languageName: node + linkType: hard + +"mjml-wrapper@npm:4.14.1": + version: 4.14.1 + resolution: "mjml-wrapper@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + lodash: ^4.17.21 + mjml-core: 4.14.1 + mjml-section: 4.14.1 + checksum: c3421fe6d783b4dfe617b37eae21aa3ff6e345ad06e18e8aeddd91e70bea75d277004feaf39d9af298e6e3ee550553df5110121d4486e1610ad51ae61a5ddf07 + languageName: node + linkType: hard + +"mjml@npm:^4.14.1": + version: 4.14.1 + resolution: "mjml@npm:4.14.1" + dependencies: + "@babel/runtime": ^7.14.6 + mjml-cli: 4.14.1 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-preset-core: 4.14.1 + mjml-validator: 4.13.0 + bin: + mjml: bin/mjml + checksum: 48906b077ea7283f77cec0baec422ebee133a5a2ea2c727c31e35f4b4e56894ef3134fb317704c12e4bc40632321779df19b950555bc49d188675e84dca7a826 + languageName: node + linkType: hard + "mkdirp@npm:1.0.4, mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -17380,6 +17912,15 @@ __metadata: languageName: node linkType: hard +"no-case@npm:^2.2.0": + version: 2.3.2 + resolution: "no-case@npm:2.3.2" + dependencies: + lower-case: ^1.1.1 + checksum: 856487731936fef44377ca74fdc5076464aba2e0734b56a4aa2b2a23d5b154806b591b9b2465faa59bb982e2b5c9391e3685400957fb4eeb38f480525adcf3dd + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -17397,7 +17938,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": +"node-fetch@npm:2, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -17512,6 +18053,13 @@ __metadata: languageName: node linkType: hard +"nodemailer@npm:^6.9.7": + version: 6.9.7 + resolution: "nodemailer@npm:6.9.7" + checksum: 0cf66d27aed3bd2cbdff9939402cec3d2119c31b2b9ff4af3bcd59f48287ea75b90c0ce2cd9eb0df838164972cd25581b4b723c91fd673e2608bcb28445ccb1b + languageName: node + linkType: hard + "noms@npm:0.0.0": version: 0.0.0 resolution: "noms@npm:0.0.0" @@ -18301,6 +18849,15 @@ __metadata: languageName: node linkType: hard +"param-case@npm:^2.1.1": + version: 2.1.1 + resolution: "param-case@npm:2.1.1" + dependencies: + no-case: ^2.2.0 + checksum: 3a63dcb8d8dc7995a612de061afdc7bb6fe7bd0e6db994db8d4cae999ed879859fd24389090e1a0d93f4c9207ebf8c048c870f468a3f4767161753e03cb9ab58 + languageName: node + linkType: hard + "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -20119,6 +20676,13 @@ __metadata: languageName: node linkType: hard +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: 4d4826e1713cbfa0f15124ab0ae494c91b597a3c458670c9714c36e8baddf5a6aad22842776f2f5b137f259c8533e741771445eb8df82e861eea37a6eaba03f7 + languageName: node + linkType: hard + "proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -20824,7 +21388,7 @@ __metadata: languageName: node linkType: hard -"react-redux@npm:^8.0.4, react-redux@npm:^8.0.5, react-redux@npm:^8.1.1": +"react-redux@npm:^8.0.5, react-redux@npm:^8.1.1": version: 8.1.2 resolution: "react-redux@npm:8.1.2" dependencies: @@ -21270,24 +21834,6 @@ __metadata: languageName: node linkType: hard -"redux-devtools-extension@npm:^2.13.5": - version: 2.13.9 - resolution: "redux-devtools-extension@npm:2.13.9" - peerDependencies: - redux: ^3.1.0 || ^4.0.0 - checksum: 603d48fd6acf3922ef373b251ab3fdbb990035e90284191047b29d25b06ea18122bc4ef01e0704ccae495acb27ab5e47b560937e98213605dd88299470025db9 - languageName: node - linkType: hard - -"redux-thunk@npm:^2.3.0, redux-thunk@npm:^2.4.2": - version: 2.4.2 - resolution: "redux-thunk@npm:2.4.2" - peerDependencies: - redux: ^4 - checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c - languageName: node - linkType: hard - "redux@npm:^4.0.0, redux@npm:^4.2.1": version: 4.2.1 resolution: "redux@npm:4.2.1" @@ -21527,13 +22073,6 @@ __metadata: languageName: node linkType: hard -"reselect@npm:^4.1.8": - version: 4.1.8 - resolution: "reselect@npm:4.1.8" - checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e - languageName: node - linkType: hard - "resize-observer-polyfill@npm:^1.5.1": version: 1.5.1 resolution: "resize-observer-polyfill@npm:1.5.1" @@ -22124,13 +22663,6 @@ __metadata: languageName: node linkType: hard -"seamless-immutable@npm:^7.1.3": - version: 7.1.4 - resolution: "seamless-immutable@npm:7.1.4" - checksum: f65c1dc12e460265ccc4b164085b807570f9fb8a619cd3c216fc7ed933fb09c57a24a7df1b638dc9bd6367d8d69c2f00b5370b0c0996b4046242539096d2d0c6 - languageName: node - linkType: hard - "section-iterator@npm:^2.0.0": version: 2.0.0 resolution: "section-iterator@npm:2.0.0" @@ -22543,6 +23075,13 @@ __metadata: languageName: node linkType: hard +"slick@npm:^1.12.2": + version: 1.12.2 + resolution: "slick@npm:1.12.2" + checksum: 02b586dac1ce12db4e6d3b89e61962e6e07966875b8099ba1d6fac2faa8c88f37450293c27706f296556e4698b9e139bfee4055b845a7c266eca4650609d7603 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -24466,6 +25005,15 @@ __metadata: languageName: node linkType: hard +"uglify-js@npm:^3.5.1": + version: 3.17.4 + resolution: "uglify-js@npm:3.17.4" + bin: + uglifyjs: bin/uglifyjs + checksum: 7b3897df38b6fc7d7d9f4dcd658599d81aa2b1fb0d074829dd4e5290f7318dbca1f4af2f45acb833b95b1fe0ed4698662ab61b87e94328eb4c0a0d3435baf924 + languageName: node + linkType: hard + "uid-safe@npm:~2.1.5": version: 2.1.5 resolution: "uid-safe@npm:2.1.5" @@ -24723,6 +25271,13 @@ __metadata: languageName: node linkType: hard +"upper-case@npm:^1.1.1": + version: 1.1.3 + resolution: "upper-case@npm:1.1.3" + checksum: 991c845de75fa56e5ad983f15e58494dd77b77cadd79d273cc11e8da400067e9881ae1a52b312aed79b3d754496e2e0712e08d22eae799e35c7f9ba6f3d8a85d + languageName: node + linkType: hard + "upper-case@npm:^2.0.2": version: 2.0.2 resolution: "upper-case@npm:2.0.2" @@ -24937,6 +25492,13 @@ __metadata: languageName: node linkType: hard +"valid-data-url@npm:^3.0.0": + version: 3.0.1 + resolution: "valid-data-url@npm:3.0.1" + checksum: 06584294fb4c9550f0aaa56470f8d748f4ebfc3ed230707db5559754719a66fc37f299b5a79b914375b8198d90f8a51e0401375391938caf8dc8e442308aab9e + languageName: node + linkType: hard + "validate-npm-package-license@npm:^3.0.1": version: 3.0.4 resolution: "validate-npm-package-license@npm:3.0.4" @@ -25268,6 +25830,20 @@ __metadata: languageName: node linkType: hard +"web-resource-inliner@npm:^6.0.1": + version: 6.0.1 + resolution: "web-resource-inliner@npm:6.0.1" + dependencies: + ansi-colors: ^4.1.1 + escape-goat: ^3.0.0 + htmlparser2: ^5.0.0 + mime: ^2.4.6 + node-fetch: ^2.6.0 + valid-data-url: ^3.0.0 + checksum: 17d9e53a6e5f07361abc584b6bb2bb8470978be580f8b5cdcab5998507ffccf5fb645616d3fe1550965d2db497f4a5cdc1ea1460c9cf464de315751962708ecc + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -26088,6 +26664,13 @@ __metadata: languageName: node linkType: hard +"yup-locales@npm:^1.2.18": + version: 1.2.18 + resolution: "yup-locales@npm:1.2.18" + checksum: e929555df880532a31973f693a0ae600522748ec08b6d0d4eff4a0d1f4af9f1d368712d74090a4b0025125e68cf95ab5d0abb9a55619f589c611181e2768dae0 + languageName: node + linkType: hard + "yup@npm:^1.3.2": version: 1.3.2 resolution: "yup@npm:1.3.2"
Voici votre code de confirmation : ${user.code}
Ce code est valable pendant 1 heure.
Veuillez le saisir dans le formulaire prévu à cet effet.
L'équipe Celluloid vous souhaite la bienvenue !
Nous avons reçu une demande de réinitialisation de mot de passe ` + - `pour l'adresse email ${user.email}
Ce code sera valable pendant 1 heure.
Si vous n'êtes pas à l'origine de cette demande, ` + - `veuillez simplement ignorer ce mail.
Cordialement,
L'équipe Celluloid