Gate link access by login level

Disable open/copy for non-allowed services based on anonymous/logged-in/admin email tiers and show an '접근 제한' tag on restricted cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-07 20:56:28 +09:00
parent 21834aa728
commit b9668a92e6
2 changed files with 71 additions and 3 deletions

View File

@@ -69,6 +69,36 @@
canManage: false,
};
// Access levels (open/copy)
const ACCESS_ANON_IDS = new Set(["dsyoon-ncue-net", "family-ncue-net", "link-ncue-net"]);
const ACCESS_USER_IDS = new Set([
"dsyoon-ncue-net",
"family-ncue-net",
"tts-ncue-net",
"meeting-ncue-net",
"link-ncue-net",
"dreamgirl-ncue-net",
]);
const ACCESS_ADMIN_EMAILS = new Set(["dosangyoon@gmail.com", "dsyoon@ncue.net"]);
function getUserEmail() {
const e = auth && auth.user && auth.user.email ? String(auth.user.email) : "";
return e.trim().toLowerCase();
}
function isAdminEmail(email) {
const e = String(email || "").trim().toLowerCase();
return ACCESS_ADMIN_EMAILS.has(e);
}
function canAccessLink(link) {
const email = getUserEmail();
if (email && isAdminEmail(email)) return true;
const id = String(link && link.id ? link.id : "");
if (email) return ACCESS_USER_IDS.has(id);
return ACCESS_ANON_IDS.has(id);
}
const auth = {
client: null,
user: null,
@@ -261,9 +291,11 @@
const desc = escapeHtml(link.description || "");
const url = escapeHtml(link.url);
const starClass = link.favorite ? "star on" : "star";
const accessible = canAccessLink(link);
const tags = (link.tags || []).slice(0, 8);
const tagHtml = [
link.favorite ? `<span class="tag fav">★ 즐겨찾기</span>` : "",
accessible ? "" : `<span class="tag lock">접근 제한</span>`,
...tags.map((t) => `<span class="tag">#${escapeHtml(t)}</span>`),
]
.filter(Boolean)
@@ -274,8 +306,20 @@
const lockTitle = state.canManage ? "" : ' title="관리 기능은 로그인 후 사용 가능합니다."';
const fav = faviconUrl(link.url);
const accessDisabledAttr = accessible ? "" : " disabled aria-disabled=\"true\"";
const accessDisabledTitle = accessible ? "" : " title=\"이 링크는 현재 권한으로 접근할 수 없습니다.\"";
const openHtml = accessible
? `<a class="btn mini" href="${url}" target="_blank" rel="noopener noreferrer" data-act="open">열기</a>`
: `<button class="btn mini" type="button"${accessDisabledAttr}${accessDisabledTitle}>열기</button>`;
const copyDisabledAttr = accessible ? "" : " disabled aria-disabled=\"true\"";
const copyDisabledTitle = accessible ? "" : " title=\"이 링크는 현재 권한으로 접근할 수 없습니다.\"";
return `
<article class="card" data-id="${escapeHtml(link.id)}">
<article class="card${accessible ? "" : " disabled"}" data-id="${escapeHtml(link.id)}" data-access="${
accessible ? "1" : "0"
}">
<div class="card-head">
<div class="card-title">
<div class="favicon" aria-hidden="true">
@@ -299,8 +343,8 @@
<div class="tags">${tagHtml || ""}</div>
<div class="card-actions">
<a class="btn mini" href="${url}" target="_blank" rel="noopener noreferrer" data-act="open">열기</a>
<button class="btn mini" type="button" data-act="copy">URL 복사</button>
${openHtml}
<button class="btn mini" type="button" data-act="copy"${copyDisabledAttr}${copyDisabledTitle}>URL 복사</button>
<button class="btn mini" type="button" data-act="edit"${lockAttr}${lockTitle}>편집</button>
<button class="btn mini mini-danger" type="button" data-act="del"${lockAttr}${lockTitle}>삭제</button>
</div>
@@ -1033,6 +1077,12 @@
if (!id) return;
const act = btn.getAttribute("data-act");
// access gate for open/copy
if ((act === "open" || act === "copy") && card.getAttribute("data-access") === "0") {
toast("이 링크는 현재 권한으로 접근할 수 없습니다.");
e.preventDefault();
return;
}
if (auth.mode === "enabled" && !state.canManage && (act === "fav" || act === "edit" || act === "del")) {
toast("관리 기능은 로그인(허용 이메일) 후 사용 가능합니다.");
return;

View File

@@ -655,10 +655,20 @@ html[data-theme="light"] .favicon .letter {
color: rgba(180, 255, 210, 0.9);
}
.tag.lock {
border-color: rgba(239, 68, 68, 0.32);
background: rgba(239, 68, 68, 0.08);
color: rgba(255, 200, 200, 0.92);
}
html[data-theme="light"] .tag.fav {
color: rgba(0, 120, 70, 0.92);
}
html[data-theme="light"] .tag.lock {
color: rgba(140, 20, 20, 0.9);
}
.card-actions {
margin-top: 12px;
display: flex;
@@ -666,6 +676,14 @@ html[data-theme="light"] .tag.fav {
flex-wrap: wrap;
}
.card.disabled {
opacity: 0.78;
}
.card.disabled:hover {
transform: none;
}
.mini {
padding: 9px 10px;
border-radius: 12px;