diff --git a/README.md b/README.md index 42445494..3fca15a8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ To get started with Docmost, please refer to our [documentation](https://docmost ## Features - Real-time collaboration +- Diagrams (Draw.io, Excalidraw and Mermaid) - Spaces - Permissions management - Groups @@ -32,4 +33,4 @@ To get started with Docmost, please refer to our [documentation](https://docmost

### Contributing -See the [development doc](https://docmost.com/docs/self-hosting/development) +See the [development documentation](https://docmost.com/docs/self-hosting/development) diff --git a/apps/client/package.json b/apps/client/package.json index ac4de98d..b2f020ca 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.3.0", + "version": "0.3.1", "scripts": { "dev": "vite", "build": "tsc && vite build", @@ -44,7 +44,6 @@ "react-error-boundary": "^4.0.13", "react-helmet-async": "^2.0.5", "react-i18next": "^15.0.1", - "react-moveable": "^0.56.0", "react-router-dom": "^6.26.1", "socket.io-client": "^4.7.5", "tippy.js": "^6.3.7", diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 049604d9..97f09d54 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -24,6 +24,8 @@ import PageRedirect from "@/pages/page/page-redirect.tsx"; import Layout from "@/components/layouts/global/layout.tsx"; import { ErrorBoundary } from "react-error-boundary"; import InviteSignup from "@/pages/auth/invite-signup.tsx"; +import ForgotPassword from "@/pages/auth/forgot-password.tsx"; +import PasswordReset from "./pages/auth/password-reset"; import { useTranslation } from "react-i18next"; export default function App() { @@ -65,6 +67,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> diff --git a/apps/client/src/components/common/recent-changes.tsx b/apps/client/src/components/common/recent-changes.tsx index c473bad6..f0eb741b 100644 --- a/apps/client/src/components/common/recent-changes.tsx +++ b/apps/client/src/components/common/recent-changes.tsx @@ -5,14 +5,15 @@ import { Badge, Table, ScrollArea, -} from "@mantine/core"; -import { Link } from "react-router-dom"; -import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx"; -import { buildPageUrl } from "@/features/page/page.utils.ts"; -import { formattedDate } from "@/lib/time.ts"; -import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts"; -import { IconFileDescription } from "@tabler/icons-react"; -import { getSpaceUrl } from "@/lib/config.ts"; + ActionIcon, +} from '@mantine/core'; +import { Link } from 'react-router-dom'; +import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx'; +import { buildPageUrl } from '@/features/page/page.utils.ts'; +import { formattedDate } from '@/lib/time.ts'; +import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts'; +import { IconFileDescription } from '@tabler/icons-react'; +import { getSpaceUrl } from '@/lib/config.ts'; import { useTranslation } from "react-i18next"; interface Props { @@ -43,7 +44,11 @@ export default function RecentChanges({ spaceId }: Props) { to={buildPageUrl(page?.space.slug, page.slugId, page.title)} > - {page.icon || } + {page.icon || ( + + + + )} {page.title || t("Untitled")} @@ -58,7 +63,7 @@ export default function RecentChanges({ spaceId }: Props) { variant="light" component={Link} to={getSpaceUrl(page?.space.slug)} - style={{ cursor: "pointer" }} + style={{ cursor: 'pointer' }} > {page?.space.name} diff --git a/apps/client/src/components/ui/error-404.tsx b/apps/client/src/components/ui/error-404.tsx index 86573298..52d4b834 100644 --- a/apps/client/src/components/ui/error-404.tsx +++ b/apps/client/src/components/ui/error-404.tsx @@ -1,19 +1,25 @@ import { Title, Text, Button, Container, Group } from "@mantine/core"; import classes from "./error-404.module.css"; import { Link } from "react-router-dom"; +import { Helmet } from "react-helmet-async"; export function Error404() { return ( - - 404 Page Not Found - - Sorry, we can't find the page you are looking for. - - - - - + <> + + 404 page not found - Docmost + + + 404 Page Not Found + + Sorry, we can't find the page you are looking for. + + + + + + ); } diff --git a/apps/client/src/features/auth/components/forgot-password-form.tsx b/apps/client/src/features/auth/components/forgot-password-form.tsx new file mode 100644 index 00000000..74714e5d --- /dev/null +++ b/apps/client/src/features/auth/components/forgot-password-form.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import * as z from "zod"; +import { useForm, zodResolver } from "@mantine/form"; +import useAuth from "@/features/auth/hooks/use-auth"; +import { IForgotPassword } from "@/features/auth/types/auth.types"; +import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core"; +import classes from "./auth.module.css"; +import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; + +const formSchema = z.object({ + email: z + .string() + .min(1, { message: "Email is required" }) + .email({ message: "Invalid email address" }), +}); + +export function ForgotPasswordForm() { + const { forgotPassword, isLoading } = useAuth(); + const [isTokenSent, setIsTokenSent] = useState(false); + useRedirectIfAuthenticated(); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + email: "", + }, + }); + + async function onSubmit(data: IForgotPassword) { + if (await forgotPassword(data)) { + setIsTokenSent(true); + } + } + + return ( + + + + Forgot password + + +
+ {!isTokenSent && ( + + )} + + {isTokenSent && ( + + A password reset link has been sent to your email. Please check + your inbox. + + )} + + {!isTokenSent && ( + + )} + +
+
+ ); +} diff --git a/apps/client/src/features/auth/components/login-form.tsx b/apps/client/src/features/auth/components/login-form.tsx index be5ad5c7..4dc1b1c1 100644 --- a/apps/client/src/features/auth/components/login-form.tsx +++ b/apps/client/src/features/auth/components/login-form.tsx @@ -1,4 +1,3 @@ -import * as React from "react"; import * as z from "zod"; import { useForm, zodResolver } from "@mantine/form"; import useAuth from "@/features/auth/hooks/use-auth"; @@ -10,9 +9,12 @@ import { Button, PasswordInput, Box, + Anchor, } from "@mantine/core"; import classes from "./auth.module.css"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import { Link, useNavigate } from "react-router-dom"; +import APP_ROUTE from "@/lib/app-route.ts"; import { useTranslation } from "react-i18next"; const formSchema = z.object({ @@ -64,10 +66,20 @@ export function LoginForm() { mt="md" {...form.getInputProps("password")} /> + + + + Forgot your password? + ); diff --git a/apps/client/src/features/auth/components/password-reset-form.tsx b/apps/client/src/features/auth/components/password-reset-form.tsx new file mode 100644 index 00000000..98964067 --- /dev/null +++ b/apps/client/src/features/auth/components/password-reset-form.tsx @@ -0,0 +1,67 @@ +import * as z from "zod"; +import { useForm, zodResolver } from "@mantine/form"; +import useAuth from "@/features/auth/hooks/use-auth"; +import { IPasswordReset } from "@/features/auth/types/auth.types"; +import { + Box, + Button, + Container, + PasswordInput, + Text, + Title, +} from "@mantine/core"; +import classes from "./auth.module.css"; +import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; + +const formSchema = z.object({ + newPassword: z + .string() + .min(8, { message: "Password must contain at least 8 characters" }), +}); + +interface PasswordResetFormProps { + resetToken?: string; +} + +export function PasswordResetForm({ resetToken }: PasswordResetFormProps) { + const { passwordReset, isLoading } = useAuth(); + useRedirectIfAuthenticated(); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + newPassword: "", + }, + }); + + async function onSubmit(data: IPasswordReset) { + await passwordReset({ + token: resetToken, + newPassword: data.newPassword + }) + } + + return ( + + + + Password reset + + +
+ + + + +
+
+ ); +} diff --git a/apps/client/src/features/auth/hooks/use-auth.ts b/apps/client/src/features/auth/hooks/use-auth.ts index ede22a04..47ba52f3 100644 --- a/apps/client/src/features/auth/hooks/use-auth.ts +++ b/apps/client/src/features/auth/hooks/use-auth.ts @@ -1,10 +1,22 @@ import { useState } from "react"; -import { login, setupWorkspace } from "@/features/auth/services/auth-service"; +import { + forgotPassword, + login, + passwordReset, + setupWorkspace, + verifyUserToken, +} from "@/features/auth/services/auth-service"; import { useNavigate } from "react-router-dom"; import { useAtom } from "jotai"; import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; -import { ILogin, ISetupWorkspace } from "@/features/auth/types/auth.types"; +import { + IForgotPassword, + ILogin, + IPasswordReset, + ISetupWorkspace, + IVerifyUserToken, +} from "@/features/auth/types/auth.types"; import { notifications } from "@mantine/notifications"; import { IAcceptInvite } from "@/features/workspace/types/workspace.types.ts"; import { acceptInvitation } from "@/features/workspace/services/workspace-service.ts"; @@ -76,6 +88,28 @@ export default function useAuth() { } }; + const handlePasswordReset = async (data: IPasswordReset) => { + setIsLoading(true); + + try { + const res = await passwordReset(data); + setIsLoading(false); + + setAuthToken(res.tokens); + + navigate(APP_ROUTE.HOME); + notifications.show({ + message: "Password reset was successful", + }); + } catch (err) { + setIsLoading(false); + notifications.show({ + message: err.response?.data.message, + color: "red", + }); + } + }; + const handleIsAuthenticated = async () => { if (!authToken) { return false; @@ -105,11 +139,50 @@ export default function useAuth() { navigate(APP_ROUTE.AUTH.LOGIN); }; + const handleForgotPassword = async (data: IForgotPassword) => { + setIsLoading(true); + + try { + await forgotPassword(data); + setIsLoading(false); + + return true; + } catch (err) { + console.log(err); + setIsLoading(false); + notifications.show({ + message: err.response?.data.message, + color: "red", + }); + + return false; + } + }; + + const handleVerifyUserToken = async (data: IVerifyUserToken) => { + setIsLoading(true); + + try { + await verifyUserToken(data); + setIsLoading(false); + } catch (err) { + console.log(err); + setIsLoading(false); + notifications.show({ + message: err.response?.data.message, + color: "red", + }); + } + }; + return { signIn: handleSignIn, invitationSignup: handleInvitationSignUp, setupWorkspace: handleSetupWorkspace, isAuthenticated: handleIsAuthenticated, + forgotPassword: handleForgotPassword, + passwordReset: handlePasswordReset, + verifyUserToken: handleVerifyUserToken, logout: handleLogout, hasTokens, isLoading, diff --git a/apps/client/src/features/auth/queries/auth-query.tsx b/apps/client/src/features/auth/queries/auth-query.tsx new file mode 100644 index 00000000..b2c52bb7 --- /dev/null +++ b/apps/client/src/features/auth/queries/auth-query.tsx @@ -0,0 +1,14 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { verifyUserToken } from "../services/auth-service"; +import { IVerifyUserToken } from "../types/auth.types"; + +export function useVerifyUserTokenQuery( + verify: IVerifyUserToken, + ): UseQueryResult { + return useQuery({ + queryKey: ["verify-token", verify], + queryFn: () => verifyUserToken(verify), + enabled: !!verify.token, + staleTime: 0, + }); + } \ No newline at end of file diff --git a/apps/client/src/features/auth/services/auth-service.ts b/apps/client/src/features/auth/services/auth-service.ts index b34b47f3..5c439724 100644 --- a/apps/client/src/features/auth/services/auth-service.ts +++ b/apps/client/src/features/auth/services/auth-service.ts @@ -1,10 +1,13 @@ import api from "@/lib/api-client"; import { IChangePassword, + IForgotPassword, ILogin, + IPasswordReset, IRegister, ISetupWorkspace, ITokenResponse, + IVerifyUserToken, } from "@/features/auth/types/auth.types"; export async function login(data: ILogin): Promise { @@ -19,15 +22,30 @@ export async function register(data: IRegister): Promise { }*/ export async function changePassword( - data: IChangePassword, + data: IChangePassword ): Promise { const req = await api.post("/auth/change-password", data); return req.data; } export async function setupWorkspace( - data: ISetupWorkspace, + data: ISetupWorkspace ): Promise { const req = await api.post("/auth/setup", data); return req.data; } + +export async function forgotPassword(data: IForgotPassword): Promise { + await api.post("/auth/forgot-password", data); +} + +export async function passwordReset( + data: IPasswordReset +): Promise { + const req = await api.post("/auth/password-reset", data); + return req.data; +} + +export async function verifyUserToken(data: IVerifyUserToken): Promise { + return api.post("/auth/verify-token", data); +} diff --git a/apps/client/src/features/auth/types/auth.types.ts b/apps/client/src/features/auth/types/auth.types.ts index 0aef0050..9ad8b2ef 100644 --- a/apps/client/src/features/auth/types/auth.types.ts +++ b/apps/client/src/features/auth/types/auth.types.ts @@ -29,3 +29,17 @@ export interface IChangePassword { oldPassword: string; newPassword: string; } + +export interface IForgotPassword { + email: string; +} + +export interface IPasswordReset { + token?: string; + newPassword: string; +} + +export interface IVerifyUserToken { + token: string; + type: string; +} diff --git a/apps/client/src/features/editor/components/code-block/code-block-view.tsx b/apps/client/src/features/editor/components/code-block/code-block-view.tsx index f01e8cc6..8ee24931 100644 --- a/apps/client/src/features/editor/components/code-block/code-block-view.tsx +++ b/apps/client/src/features/editor/components/code-block/code-block-view.tsx @@ -49,7 +49,7 @@ export default function CodeBlockView(props: NodeViewProps) {