Skip to content

Commit

Permalink
🐛 Fix file duplication when creating from file tree
Browse files Browse the repository at this point in the history
  • Loading branch information
vadolasi committed Sep 18, 2024
1 parent dc3d128 commit 3f12294
Show file tree
Hide file tree
Showing 22 changed files with 547 additions and 375 deletions.
8 changes: 8 additions & 0 deletions apps/server/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class HTTPError extends Error {
constructor(
public status: number,
public message: string,
) {
super(message);
}
}
11 changes: 11 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Elysia } from "elysia";
import { helmet } from "elysia-helmet";
import { msgpack } from "elysia-msgpack";
import env from "./env";
import { HTTPError } from "./error";
import { authController } from "./modules/auth/auth.controller";
import authMiddleware from "./modules/auth/auth.middleware";
import { usersController } from "./modules/users/users.controller";
Expand All @@ -22,6 +23,16 @@ const app = new Elysia({
},
})
.use(msgpack({ moreTypes: true }))
.error({
HTTPError,
})
.onError(({ code, error, set }) => {
switch (code) {
case "HTTPError":
set.status = error.status;
throw new Error(error.message);
}
})
.use(helmet())
.use(authMiddleware)
.use(authController)
Expand Down
12 changes: 1 addition & 11 deletions apps/server/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ export const authController = new Elysia({ prefix: "/auth" })
async ({ body: { email }, cookie: { session: sessionCookie } }) => {
const result = await authService.resendEmail({ email });

if (result.error) {
return result;
}

sessionCookie.set({
value: result.value,
expires: result.attributes.expires,
Expand Down Expand Up @@ -52,17 +48,11 @@ export const authController = new Elysia({ prefix: "/auth" })
cookie: { session: sessionCookie },
set,
}) => {
const result = await authService.login({
const { user, cookie } = await authService.login({
email,
password,
});

if (result.error) {
return result;
}

const { user, cookie } = result;

sessionCookie.set({
value: cookie.value,
expires: cookie.attributes.expires,
Expand Down
23 changes: 12 additions & 11 deletions apps/server/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
usersTable,
} from "../../db/schema";
import env from "../../env";
import { HTTPError } from "../../error";
import EmailsService, { templates } from "../email/emails.service";
import { lucia } from "./auth.utils";

Expand Down Expand Up @@ -74,19 +75,19 @@ export default class AuthService {
});

if (!user) {
return error(404, "User not found");
throw new HTTPError(404, "User not found");
}

if (user.emailVerified) {
return error(400, "Email already confirmed");
throw new HTTPError(400, "Email already confirmed");
}

return await this.sendEmailVerificationCode(email, user.id);
}

async confirmEmail({ user, code }: { user: User | null; code: string }) {
if (user === null) {
return error(401, "Unauthorized");
throw new HTTPError(401, "Unauthorized");
}

const databaseCode = await db.query.emailVerificationCodeTable.findFirst({
Expand All @@ -99,15 +100,15 @@ export default class AuthService {
databaseCode.code !== code ||
databaseCode.email !== user.email
) {
return error(400, "Invalid code");
throw new HTTPError(400, "Invalid code");
}

await db
.delete(emailVerificationCodeTable)
.where(eq(emailVerificationCodeTable.userId, user.id));

if (!isWithinExpirationDate(new Date(databaseCode.expiresAt))) {
return error(400, "Code expired");
throw new HTTPError(400, "Code expired");
}

await db
Expand All @@ -124,11 +125,11 @@ export default class AuthService {
});

if (!user) {
return error(401, "Invalid email or password");
throw new HTTPError(401, "Invalid email or password");
}

if (!user.emailVerified) {
return error(401, "Email not confirmed");
throw new HTTPError(401, "Email not confirmed");
}

if (
Expand All @@ -137,7 +138,7 @@ export default class AuthService {
user.password,
)) === false
) {
return error(401, "Invalid email or password");
throw new HTTPError(401, "Invalid email or password");
}

const session = await lucia.createSession(user.id, {});
Expand All @@ -152,7 +153,7 @@ export default class AuthService {
});

if (!user) {
return error(404, "User not found");
throw new HTTPError(404, "User not found");
}

const token = await this.createPasswordResetToken(email);
Expand Down Expand Up @@ -180,11 +181,11 @@ export default class AuthService {
);

if (!passwordResetToken) {
return error(400, "Invalid token");
throw new HTTPError(400, "Invalid token");
}

if (!isWithinExpirationDate(new Date(passwordResetToken.expiresAt))) {
return error(400, "Token expired");
throw new HTTPError(400, "Token expired");
}

await db
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/modules/email/emails.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Resend } from "resend";
import EmailConfirmation from "transactional/emails/EmailConfirmation";
import ResetPassword from "transactional/emails/ResetPassword";
import env from "../../env";
import { HTTPError } from "../../error";

export default class EmailsService {
private resend = new Resend(env.RESEND_API_KEY);
Expand All @@ -17,7 +18,7 @@ export default class EmailsService {
});

if (error) {
throw new InternalServerError("Failed to send email");
throw new HTTPError(500, "Failed to send email");
}
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
"@xterm/xterm": "^5.5.0",
"clsx": "^2.1.1",
"codemirror": "^6.0.1",
"file-extension-icon-js": "^1.1.6",
"goober": "^2.1.14",
"immer": "^10.1.1",
"loro-crdt": "^0.16.7",
"million": "^3.1.0",
"msgpackr": "^1.10.2",
"path-browserify": "^1.0.1",
"randomcolor": "^0.6.2",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
Expand All @@ -40,6 +42,7 @@
"react-hook-form": "^7.51.4",
"react-hot-toast": "^2.4.1",
"react-resizable-panels": "^2.0.19",
"rrweb": "^2.0.0-alpha.4",
"server": "workspace:*",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1",
Expand All @@ -50,6 +53,7 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/path-browserify": "^1.0.3",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { keyframes } from "goober";
import { Suspense, useEffect } from "react";
import { Toaster, resolveValue } from "react-hot-toast";
import { Redirect, Route, Switch, useLocation } from "wouter";

import importedRoutes from "~react-pages";
import LoadingIndicator from "./components/LoadingIndicator";
import cn from "./utils/cn";
Expand Down
6 changes: 1 addition & 5 deletions apps/web/src/components/Editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { nord } from "@uiw/codemirror-theme-nord";
import CodeMirror, { type Extension } from "@uiw/react-codemirror";

const Editor: React.FC<{ extensions: Extension[]; initialValue: string }> = ({
extensions,
initialValue,
}) => {
const Editor: React.FC<{ extensions: Extension[] }> = ({ extensions }) => {
return (
<CodeMirror
theme="dark"
height="100%"
className="h-full"
value={initialValue}
extensions={[...extensions, nord]}
/>
);
Expand Down
11 changes: 7 additions & 4 deletions apps/web/src/components/Editor/plugins/collab/cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
type ViewUpdate,
WidgetType,
} from "@codemirror/view";
import type { Loro } from "loro-crdt";
import randomColor from "randomcolor";
import type Codelabs from "../../../../core";
import useStore from "../../../../utils/store";

const cursorsTheme = EditorView.baseTheme({
Expand Down Expand Up @@ -133,8 +133,11 @@ class RemoteCaretWidget extends WidgetType {
}
}

export default function collabCursorsPlugin(doc: Loro, currentTab: string) {
const presence = doc.getMap("presence");
export default function collabCursorsPlugin(
codelabs: Codelabs,
currentTab: string,
) {
const presence = codelabs.doc.getMap("presence");
const user = useStore.getState().user;
const userId = user?.id ?? "";
const userName = `${user?.firstName} ${user?.lastName}`;
Expand Down Expand Up @@ -258,7 +261,7 @@ export default function collabCursorsPlugin(doc: Loro, currentTab: string) {
(presence.get(userId) as { color: string } | undefined)?.color ??
randomColor({ luminosity: "light" }),
});
doc.commit();
codelabs.doc.commit();
}
}

Expand Down
13 changes: 4 additions & 9 deletions apps/web/src/components/Editor/plugins/collab/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { type Loro, LoroText } from "loro-crdt";
import type Codelabs from "../../../../core";
import collabCursorsPlugin from "./cursors";
import collabTextPlugin from "./loro";

const collab = (doc: Loro, path: string, userId: string) => {
const docText = doc
.getTree("fileTree")
.getNodeByID(path as `${number}@${number}`)
.data.getOrCreateContainer("content", new LoroText());

const collab = (codelabs: Codelabs, path: string, userId: string) => {
return [
collabTextPlugin(doc, docText, userId),
collabCursorsPlugin(doc, path),
collabTextPlugin(codelabs, path, userId),
collabCursorsPlugin(codelabs, path),
];
};

Expand Down
37 changes: 32 additions & 5 deletions apps/web/src/components/Editor/plugins/collab/loro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@ import {
ViewPlugin,
type ViewUpdate,
} from "@codemirror/view";
import type { Loro, LoroText } from "loro-crdt";
import { LoroText } from "loro-crdt";
import type Codelabs from "../../../../core";

export default function collabTextPlugin(
loroDoc: Loro,
loroText: LoroText,
codelabs: Codelabs,
path: string,
userId: string,
) {
const loroText = codelabs.docTree
.getNodeByID(path as `${number}@${number}`)
.data.getOrCreateContainer("content", new LoroText());

class LoroPluginClass implements PluginValue {
private annotation = Annotation.define();
private loroSubscription: number;

constructor(view: EditorView) {
loroText.subscribe(({ by, events }) => {
this.loroSubscription = loroText.subscribe(({ by, events }) => {
if (by !== "local") {
for (const event of events) {
if (event.diff.type === "text") {
Expand All @@ -42,8 +48,25 @@ export default function collabTextPlugin(
}
}
}
} else {
const currentText = loroText.toString();
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: currentText,
},
annotations: [this.annotation.of(userId)],
});
}
});
setTimeout(() => {
const currentText = loroText.toString();
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: currentText },
annotations: [this.annotation.of(userId)],
});
}, 100);
}

update(update: ViewUpdate) {
Expand All @@ -64,9 +87,13 @@ export default function collabTextPlugin(
}
adj += insertText.length - (toA - fromA);
});
loroDoc.commit();
codelabs.doc.commit(`editor-${path}`);
}
}

destroy() {
loroText.unsubscribe(this.loroSubscription);
}
}

return [ViewPlugin.fromClass(LoroPluginClass)];
Expand Down
Loading

0 comments on commit 3f12294

Please sign in to comment.