Skip to content

Commit

Permalink
add journal name validation
Browse files Browse the repository at this point in the history
- add basic validation for journal names (size, url safe, somewhat file system safe)
- re-work JournalResponse type to avoid import 'path' issue
  • Loading branch information
cloverich committed Sep 14, 2024
1 parent 73d4306 commit 2bfa8e5
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 23 deletions.
8 changes: 6 additions & 2 deletions src/hooks/useClient.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React, { useContext } from "react";
import { IClient } from "../preload/client/types";
export { IClient } from "../preload/client/types";

export { SearchResponse } from "../preload/client/documents";
export { JournalResponse } from "../preload/client/journals";
export { IClient, JournalResponse } from "../preload/client/types";

export const ClientContext = React.createContext<any>(
(window as any).chronicles.createClient(),
);
ClientContext.displayName = "ClientContext";

/**
* Hook to get the client that separates "server side" from the UI.
*
* Note that this is the only safe place for UI code to access the client.
*/
export default function useClient(): IClient {
return useContext(ClientContext);
}
3 changes: 1 addition & 2 deletions src/hooks/useJournalsLoader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from "react";
import { JournalResponse } from "../preload/client/journals";
import { JournalsStore } from "./stores/journals";
import useClient from "./useClient";
import useClient, { JournalResponse } from "./useClient";

export const JournalsStoreContext = React.createContext<JournalsStore | null>(
null,
Expand Down
50 changes: 40 additions & 10 deletions src/preload/client/journals.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { Database } from "better-sqlite3";
import path from "path";
import { uuidv7 } from "uuidv7";

export interface JournalResponse {
id: string;
name: string;
createdAt: string;
updatedAt: string;
archivedAt: string;
}
import { JournalResponse } from "./types";

export type IJournalsClient = JournalsClient;

Expand All @@ -19,15 +14,16 @@ export class JournalsClient {
};

create = (journal: { name: string }): Promise<JournalResponse> => {
const name = validateJournalName(journal.name);
const id = uuidv7();

this.db
.prepare(
"insert into journals (id, name, createdAt, updatedAt) values (:id, :name, :createdAt, :updatedAt)",
)
.run({
name: journal.name,
id,
name,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand All @@ -39,14 +35,14 @@ export class JournalsClient {
id: string;
name: string;
}): Promise<JournalResponse> => {
if (!journal.name?.trim()) throw new Error("Journal name cannot be empty");
const name = validateJournalName(journal.name);

this.db
.prepare(
"update journals set name = :name, updatedAt = :updatedAt where id = :id",
)
.run({
name: journal.name,
name,
id: journal.id,
updatedAt: new Date().toISOString(),
});
Expand Down Expand Up @@ -85,3 +81,37 @@ export class JournalsClient {
return this.list();
};
}

const MAX_NAME_LENGTH = 20;

/**
* A basic validation function for journal names.
*/
const validateJournalName = (name: string): string => {
name = name?.trim() || "";
if (!name) {
throw new Error("Journal name cannot be empty.");
}

// Check for max length
if (name.length > MAX_NAME_LENGTH) {
throw new Error(
`Journal name exceeds max length of ${MAX_NAME_LENGTH} characters.`,
);
}

let sanitized = decodeURIComponent(encodeURIComponent(name));

// Check for URL safety
if (name !== sanitized) {
throw new Error("Journal name is not URL safe.");
}

// Ensure the name doesn't contain path traversal or invalid slashes
sanitized = path.basename(name);
if (sanitized !== name) {
throw new Error("Journal name contains invalid path characters.");
}

return sanitized;
};
2 changes: 0 additions & 2 deletions src/preload/client/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ export class PreferencesClient {
return settingsJson as unknown as Preferences;
};

// todo: Ideally this could go into a preload script
// see the main script (electron/index) for the other half
openDialog = () => {
ipcRenderer.send("select-database-file");
};
Expand Down
8 changes: 8 additions & 0 deletions src/preload/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ export interface IClient {
preferences: IPreferencesClient;
files: IFilesClient;
}

export type JournalResponse = {
id: string;
name: string;
createdAt: string;
updatedAt: string;
archivedAt: string;
};
2 changes: 1 addition & 1 deletion src/views/edit/FrontMatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DayPicker } from "react-day-picker";
import * as D from "../../components/DropdownMenu";
import * as Popover from "../../components/Popover";
import TagInput from "../../components/TagInput";
import { JournalResponse } from "../../preload/client/journals";
import { JournalResponse } from "../../hooks/useClient";
import { TagTokenParser } from "../documents/search/parsers/tag";
import { EditableDocument } from "./EditableDocument";

Expand Down
8 changes: 3 additions & 5 deletions src/views/edit/PlateContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ import {
createVideoPlugin,
} from "./editor/plugins/createVideoPlugin";

import { JournalResponse } from "../../hooks/useClient";
import useClient, { JournalResponse } from "../../hooks/useClient";
import { JournalsStoreContext } from "../../hooks/useJournalsLoader";
import { SearchStore } from "../documents/SearchStore";
import { EditableDocument } from "./EditableDocument";
import { EditorMode } from "./EditorMode";

Expand All @@ -116,10 +118,6 @@ export interface Props {
setSelectedEditorMode: (s: EditorMode) => any;
}

import useClient from "../../hooks/useClient";
import { JournalsStoreContext } from "../../hooks/useJournalsLoader";
import { SearchStore } from "../documents/SearchStore";

export default observer(
({ children, saving, value, setValue }: React.PropsWithChildren<Props>) => {
const jstore = useContext(JournalsStoreContext);
Expand Down
2 changes: 1 addition & 1 deletion src/views/edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ChevronLeftIcon, IconButton, Pane } from "evergreen-ui";
import { observer } from "mobx-react-lite";
import React, { useContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { JournalResponse } from "../../hooks/useClient";
import { JournalsStoreContext } from "../../hooks/useJournalsLoader";
import { JournalResponse } from "../../preload/client/journals";
import Titlebar from "../../titlebar/macos";
import { useSearchStore } from "../documents/SearchStore";
import * as Base from "../layout";
Expand Down

0 comments on commit 2bfa8e5

Please sign in to comment.