diff --git a/backend/main.py b/backend/main.py
index cf466cd..71923c7 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -67,7 +67,7 @@ def init_db():
cur.execute("""
create table if not exists links(
id bigint auto_increment primary key,
- name varchar(255) not null,
+ name varchar(255) not null unique,
url text not null,
description text,
category varchar(255),
@@ -113,6 +113,15 @@ def require_admin(request: Request):
raise HTTPException(401, "Unauthorized")
return user
+
+def link_name_exists(conn, name: str, *, exclude_id: int | None = None) -> bool:
+ with conn.cursor() as cur:
+ if exclude_id is None:
+ cur.execute("select 1 from links where name=%s limit 1", (name,))
+ else:
+ cur.execute("select 1 from links where name=%s and id<>%s limit 1", (name, exclude_id))
+ return cur.fetchone() is not None
+
@app.get("/api/me")
def me(request: Request):
with db() as c:
@@ -197,6 +206,9 @@ def create_link(
icon: UploadFile | None = File(None),
):
require_admin(request)
+ with db() as c:
+ if link_name_exists(c, name):
+ raise HTTPException(409, "Link name already exists")
icon_blob = icon_mime = None
if icon:
icon_blob = icon.file.read()
@@ -246,6 +258,8 @@ def update_link(
icon_mime = icon.content_type
now = datetime.utcnow().isoformat()
with db() as c:
+ if link_name_exists(c, name, exclude_id=link_id):
+ raise HTTPException(409, "Link name already exists")
with c.cursor() as cur:
if icon_blob:
cur.execute("""update links set name=%s,url=%s,description=%s,category=%s,icon_blob=%s,icon_mime=%s,icon_url=%s,enabled=%s,updated_at=%s where id=%s""",
diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx
index 9f7ee50..3c1873f 100644
--- a/frontend/src/app.tsx
+++ b/frontend/src/app.tsx
@@ -50,6 +50,14 @@ export default function App() {
() => links.filter((l) => l.enabled && `${l.name} ${l.description} ${l.category}`.toLowerCase().includes(query.toLowerCase())),
[links, query]
);
+ const categories = useMemo(() => {
+ const base = ['All'];
+ const visible = links
+ .filter((link) => link.enabled)
+ .map((link) => link.category)
+ .filter((value, index, array) => value && array.indexOf(value) === index);
+ return base.concat(visible.sort((a, b) => a.localeCompare(b)));
+ }, [links]);
if (page === 'loading') return