feat: update shortcut redirector

prod
Steven 2 years ago
parent 6b3ff5e462
commit 700598d1a5

@ -1,18 +0,0 @@
package api
type ShortcutOrganizer struct {
ShortcutID int
UserID int
Pinned bool
}
type ShortcutOrganizerFind struct {
ShortcutID int
UserID int
}
type ShortcutOrganizerUpsert struct {
ShortcutID int
UserID int
Pinned bool `json:"pinned"`
}

@ -80,7 +80,7 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
} }
} }
if common.HasPrefixes(path, "/api/ping", "/api/status") && c.Request().Method == http.MethodGet { if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/workspace") && c.Request().Method == http.MethodGet {
return next(c) return next(c)
} }

@ -26,6 +26,17 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
} }
existingShortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{
Name: &shortcutCreate.Name,
WorkspaceID: &shortcutCreate.WorkspaceID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
}
if existingShortcut != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Shortcut with name %s already exists", shortcutCreate.Name))
}
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate) shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
@ -48,6 +59,29 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
} }
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
shortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{
ID: &shortcutID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
}
workspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{
UserID: &userID,
WorkspaceID: &shortcut.WorkspaceID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err)
}
if shortcut.CreatorID != userID && workspaceUser.Role != api.RoleAdmin {
return echo.NewHTTPError(http.StatusForbidden, "Forbidden to patch shortcut")
}
shortcutPatch := &api.ShortcutPatch{ shortcutPatch := &api.ShortcutPatch{
ID: shortcutID, ID: shortcutID,
@ -56,7 +90,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
} }
shortcut, err := s.Store.PatchShortcut(ctx, shortcutPatch) shortcut, err = s.Store.PatchShortcut(ctx, shortcutPatch)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
} }

@ -70,7 +70,7 @@ CREATE TABLE shortcut (
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
workspace_id INTEGER NOT NULL, workspace_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
link TEXT NOT NULL DEFAULT '', link TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE' visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
); );
@ -78,12 +78,4 @@ CREATE TABLE shortcut (
INSERT INTO INSERT INTO
sqlite_sequence (name, seq) sqlite_sequence (name, seq)
VALUES VALUES
('shortcut', 1000); ('shortcut', 1000);
-- shortcut_organizer
CREATE TABLE shortcut_organizer (
shortcut_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(shortcut_id, user_id)
);

@ -70,7 +70,7 @@ CREATE TABLE shortcut (
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
workspace_id INTEGER NOT NULL, workspace_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
link TEXT NOT NULL DEFAULT '', link TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE' visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
); );
@ -78,12 +78,4 @@ CREATE TABLE shortcut (
INSERT INTO INSERT INTO
sqlite_sequence (name, seq) sqlite_sequence (name, seq)
VALUES VALUES
('shortcut', 1000); ('shortcut', 1000);
-- shortcut_organizer
CREATE TABLE shortcut_organizer (
shortcut_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(shortcut_id, user_id)
);

@ -1,6 +1,3 @@
DELETE FROM
shortcut_organizer;
DELETE FROM DELETE FROM
shortcut; shortcut;

@ -1,11 +1,14 @@
import { Tooltip } from "@mui/joy"; import { Tooltip } from "@mui/joy";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import { useState } from "react"; import { useState } from "react";
import { UNKNOWN_ID } from "../helpers/consts";
import { shortcutService, workspaceService } from "../services"; import { shortcutService, workspaceService } from "../services";
import { useAppSelector } from "../store"; import { useAppSelector } from "../store";
import { UNKNOWN_ID } from "../helpers/consts"; import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace";
import { absolutifyLink } from "../helpers/utils";
import { showCommonDialog } from "./Alert"; import { showCommonDialog } from "./Alert";
import Icon from "./Icon"; import Icon from "./Icon";
import toastHelper from "./Toast";
import Dropdown from "./common/Dropdown"; import Dropdown from "./common/Dropdown";
import CreateShortcutDialog from "./CreateShortcutDialog"; import CreateShortcutDialog from "./CreateShortcutDialog";
@ -20,14 +23,22 @@ interface State {
const ShortcutListView: React.FC<Props> = (props: Props) => { const ShortcutListView: React.FC<Props> = (props: Props) => {
const { workspaceId, shortcutList } = props; const { workspaceId, shortcutList } = props;
const { user } = useAppSelector((state) => state.user); const user = useAppSelector((state) => state.user.user as User);
const { workspaceList } = useAppSelector((state) => state.workspace);
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
currentEditingShortcutId: UNKNOWN_ID, currentEditingShortcutId: UNKNOWN_ID,
}); });
const workspace = workspaceList.find((workspace) => workspace.id === workspaceId) ?? unknownWorkspace;
const workspaceUser = workspace.workspaceUserList.find((workspaceUser) => workspaceUser.userId === user.id) ?? unknownWorkspaceUser;
const havePermission = (shortcut: Shortcut) => {
return workspaceUser.role === "ADMIN" || shortcut.creatorId === user.id;
};
const handleCopyButtonClick = (shortcut: Shortcut) => { const handleCopyButtonClick = (shortcut: Shortcut) => {
const workspace = workspaceService.getWorkspaceById(workspaceId); const workspace = workspaceService.getWorkspaceById(workspaceId);
copy(`${location.host}/${workspace?.name}/go/${shortcut.name}`); copy(absolutifyLink(`/${workspace?.name}/${shortcut.name}`));
toastHelper.error("Shortcut link copied to clipboard.");
}; };
const handleEditShortcutButtonClick = (shortcut: Shortcut) => { const handleEditShortcutButtonClick = (shortcut: Shortcut) => {
@ -79,14 +90,14 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
actions={ actions={
<> <>
<button <button
disabled={shortcut.creatorId !== user?.id} disabled={!havePermission(shortcut)}
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60" className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => handleEditShortcutButtonClick(shortcut)} onClick={() => handleEditShortcutButtonClick(shortcut)}
> >
Edit Edit
</button> </button>
<button <button
disabled={shortcut.creatorId !== user?.id} disabled={!havePermission(shortcut)}
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60" className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => { onClick={() => {
handleDeleteShortcutButtonClick(shortcut); handleDeleteShortcutButtonClick(shortcut);

@ -4,49 +4,8 @@ export const isNullorUndefined = (value: any) => {
return isNull(value) || isUndefined(value); return isNull(value) || isUndefined(value);
}; };
export function getNowTimeStamp(): number { export function absolutifyLink(rel: string): string {
return Date.now(); const anchor = document.createElement("a");
} anchor.setAttribute("href", rel);
return anchor.href;
export function getOSVersion(): "Windows" | "MacOS" | "Linux" | "Unknown" {
const appVersion = navigator.userAgent;
let detectedOS: "Windows" | "MacOS" | "Linux" | "Unknown" = "Unknown";
if (appVersion.indexOf("Win") != -1) {
detectedOS = "Windows";
} else if (appVersion.indexOf("Mac") != -1) {
detectedOS = "MacOS";
} else if (appVersion.indexOf("Linux") != -1) {
detectedOS = "Linux";
}
return detectedOS;
}
export function debounce(fn: FunctionType, delay: number) {
let timer: number | null = null;
return () => {
if (timer) {
clearTimeout(timer);
timer = setTimeout(fn, delay);
} else {
timer = setTimeout(fn, delay);
}
};
}
export function throttle(fn: FunctionType, delay: number) {
let valid = true;
return () => {
if (!valid) {
return false;
}
valid = false;
setTimeout(() => {
fn();
valid = true;
}, delay);
};
} }

@ -1,37 +1,35 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Header from "../components/Header"; import Header from "../components/Header";
import { getShortcutWithNameAndWorkspaceName } from "../helpers/api"; import { getShortcutWithNameAndWorkspaceName } from "../helpers/api";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import { userService } from "../services";
interface State { interface State {
errMessage?: string; errMessage?: string;
} }
const ShortcutRedirector: React.FC = () => { const ShortcutRedirector: React.FC = () => {
const navigate = useNavigate();
const params = useParams(); const params = useParams();
const [state, setState] = useState<State>(); const [state, setState] = useState<State>();
const loadingState = useLoading(); const loadingState = useLoading();
useEffect(() => { useEffect(() => {
if (!userService.getState().user) {
navigate("/user/auth");
return;
}
const workspaceName = params.workspaceName || ""; const workspaceName = params.workspaceName || "";
const shortcutName = params.shortcutName || ""; const shortcutName = params.shortcutName || "";
getShortcutWithNameAndWorkspaceName(workspaceName, shortcutName) getShortcutWithNameAndWorkspaceName(workspaceName, shortcutName)
.then(({ data: { data: shortcut } }) => { .then(({ data: { data: shortcut } }) => {
if (shortcut) { if (shortcut) {
window.location.href = shortcut.link; window.location.href = shortcut.link;
} else {
setState({
errMessage: "Not found",
});
loadingState.setFinish();
} }
}) })
.catch((err) => { .catch((error) => {
setState({ setState({
errMessage: err?.message || "Not found", errMessage: error.response.data.error || "Error occurred",
}); });
loadingState.setFinish(); loadingState.setFinish();
}); });

@ -62,21 +62,8 @@ const router = createBrowserRouter([
}, },
}, },
{ {
path: "/:workspaceName/go/:shortcutName", path: "/:workspaceName/:shortcutName",
element: <ShortcutRedirector />, element: <ShortcutRedirector />,
loader: async () => {
try {
await userService.initialState();
await workspaceService.fetchWorkspaceList();
} catch (error) {
// do nth
}
const { user } = userService.getState();
if (isNullorUndefined(user)) {
return redirect("/user/auth");
}
},
}, },
]); ]);

Loading…
Cancel
Save