mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-16 12:23:12 +00:00
feat: update shortcut redirector
This commit is contained in:
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)
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,17 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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{
|
||||
ID: shortcutID,
|
||||
@ -56,7 +90,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
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 {
|
||||
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',
|
||||
workspace_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
link TEXT NOT NULL DEFAULT '',
|
||||
link TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||
);
|
||||
@ -78,12 +78,4 @@ CREATE TABLE shortcut (
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('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)
|
||||
);
|
||||
('shortcut', 1000);
|
@ -70,7 +70,7 @@ CREATE TABLE shortcut (
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
workspace_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
link TEXT NOT NULL DEFAULT '',
|
||||
link TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||
);
|
||||
@ -78,12 +78,4 @@ CREATE TABLE shortcut (
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('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)
|
||||
);
|
||||
('shortcut', 1000);
|
@ -1,6 +1,3 @@
|
||||
DELETE FROM
|
||||
shortcut_organizer;
|
||||
|
||||
DELETE FROM
|
||||
shortcut;
|
||||
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useState } from "react";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { shortcutService, workspaceService } from "../services";
|
||||
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 Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import CreateShortcutDialog from "./CreateShortcutDialog";
|
||||
|
||||
@ -20,14 +23,22 @@ interface State {
|
||||
|
||||
const ShortcutListView: React.FC<Props> = (props: 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>({
|
||||
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 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) => {
|
||||
@ -79,14 +90,14 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
|
||||
actions={
|
||||
<>
|
||||
<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"
|
||||
onClick={() => handleEditShortcutButtonClick(shortcut)}
|
||||
>
|
||||
Edit
|
||||
</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"
|
||||
onClick={() => {
|
||||
handleDeleteShortcutButtonClick(shortcut);
|
||||
|
@ -4,49 +4,8 @@ export const isNullorUndefined = (value: any) => {
|
||||
return isNull(value) || isUndefined(value);
|
||||
};
|
||||
|
||||
export function getNowTimeStamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
export function absolutifyLink(rel: string): string {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.setAttribute("href", rel);
|
||||
return anchor.href;
|
||||
}
|
||||
|
@ -1,37 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import Header from "../components/Header";
|
||||
import { getShortcutWithNameAndWorkspaceName } from "../helpers/api";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { userService } from "../services";
|
||||
|
||||
interface State {
|
||||
errMessage?: string;
|
||||
}
|
||||
|
||||
const ShortcutRedirector: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const [state, setState] = useState<State>();
|
||||
const loadingState = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
if (!userService.getState().user) {
|
||||
navigate("/user/auth");
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceName = params.workspaceName || "";
|
||||
const shortcutName = params.shortcutName || "";
|
||||
getShortcutWithNameAndWorkspaceName(workspaceName, shortcutName)
|
||||
.then(({ data: { data: shortcut } }) => {
|
||||
if (shortcut) {
|
||||
window.location.href = shortcut.link;
|
||||
} else {
|
||||
setState({
|
||||
errMessage: "Not found",
|
||||
});
|
||||
loadingState.setFinish();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch((error) => {
|
||||
setState({
|
||||
errMessage: err?.message || "Not found",
|
||||
errMessage: error.response.data.error || "Error occurred",
|
||||
});
|
||||
loadingState.setFinish();
|
||||
});
|
||||
|
@ -62,21 +62,8 @@ const router = createBrowserRouter([
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/:workspaceName/go/:shortcutName",
|
||||
path: "/:workspaceName/:shortcutName",
|
||||
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…
x
Reference in New Issue
Block a user