dashboard v1
This commit is contained in:
1 parent
57d420a9f4
commit
22c33c0f7a
66 files changed
+6100
-465
No files matched your search
@@ -0,0 +1,291 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
CONFIG_FILES,
|
||||
ROLES,
|
||||
fullPermissions,
|
||||
permissionsFromRole,
|
||||
normalizePermissions,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
} = require("./DashboardPermissions.js");
|
||||
const { discordAvatarUrl } = require("./DiscordOAuth.js");
|
||||
|
||||
const STORE_PATH = path.join(__dirname, "../Data/dashboard-users.json");
|
||||
|
||||
function ensureDataDir() {
|
||||
const dir = path.dirname(STORE_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function emptyStore() {
|
||||
return { version: 1, users: {} };
|
||||
}
|
||||
|
||||
function readStore() {
|
||||
ensureDataDir();
|
||||
if (!fs.existsSync(STORE_PATH)) return emptyStore();
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(STORE_PATH, "utf8"));
|
||||
if (!raw.users || typeof raw.users !== "object") return emptyStore();
|
||||
return raw;
|
||||
} catch {
|
||||
return emptyStore();
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(store) {
|
||||
ensureDataDir();
|
||||
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function getOwnerIds(oauth) {
|
||||
const owners = new Set();
|
||||
for (const id of oauth?.OwnerUserIds || []) owners.add(String(id));
|
||||
for (const id of oauth?.AllowedUserIds || []) owners.add(String(id));
|
||||
return owners;
|
||||
}
|
||||
|
||||
function isYamlOwner(userId, oauth) {
|
||||
return getOwnerIds(oauth).has(String(userId));
|
||||
}
|
||||
|
||||
function bootstrapFromOAuth(oauth) {
|
||||
const store = readStore();
|
||||
const owners = getOwnerIds(oauth);
|
||||
let changed = false;
|
||||
|
||||
for (const id of owners) {
|
||||
if (!store.users[id]) {
|
||||
store.users[id] = {
|
||||
id,
|
||||
username: null,
|
||||
globalName: null,
|
||||
avatar: null,
|
||||
role: "owner",
|
||||
permissions: null,
|
||||
enabled: true,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: "system",
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) writeStore(store);
|
||||
return store;
|
||||
}
|
||||
|
||||
function syncDiscordProfile(userId, profile) {
|
||||
const store = readStore();
|
||||
const entry = store.users[userId];
|
||||
if (!entry) return;
|
||||
|
||||
entry.username = profile.username ?? entry.username;
|
||||
entry.globalName = profile.globalName ?? entry.globalName;
|
||||
entry.avatar = profile.avatar ?? entry.avatar;
|
||||
entry.updatedAt = Date.now();
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
function resolvePermissions(entry, oauth, userId) {
|
||||
if (isYamlOwner(userId, oauth)) {
|
||||
return { permissions: fullPermissions(), role: "owner", isOwner: true };
|
||||
}
|
||||
if (!entry?.enabled) return null;
|
||||
|
||||
const role = entry.role || "viewer";
|
||||
const permissions =
|
||||
role === "custom"
|
||||
? permissionsFromRole("custom", entry.permissions)
|
||||
: permissionsFromRole(role);
|
||||
|
||||
return { permissions, role, isOwner: false };
|
||||
}
|
||||
|
||||
function getAccess(userId, oauth) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const id = String(userId);
|
||||
|
||||
if (isYamlOwner(id, oauth)) {
|
||||
const entry = store.users[id];
|
||||
return {
|
||||
entry: entry || { id, role: "owner" },
|
||||
permissions: fullPermissions(),
|
||||
role: "owner",
|
||||
isOwner: true,
|
||||
yamlOwner: true,
|
||||
};
|
||||
}
|
||||
|
||||
const entry = store.users[id];
|
||||
const resolved = resolvePermissions(entry, oauth, id);
|
||||
if (!resolved) return null;
|
||||
|
||||
return {
|
||||
entry,
|
||||
permissions: resolved.permissions,
|
||||
role: resolved.role,
|
||||
isOwner: false,
|
||||
yamlOwner: false,
|
||||
};
|
||||
}
|
||||
|
||||
function canLogin(userId, oauth) {
|
||||
return Boolean(getAccess(userId, oauth));
|
||||
}
|
||||
|
||||
function formatPublicUser(entry, access, discordProfile) {
|
||||
const id = String(entry?.id || discordProfile?.id);
|
||||
const avatarHash = entry?.avatar ?? discordProfile?.avatar ?? null;
|
||||
|
||||
return {
|
||||
id,
|
||||
username: entry?.username || discordProfile?.username || "Unknown",
|
||||
globalName: entry?.globalName ?? discordProfile?.global_name ?? null,
|
||||
avatar: avatarHash
|
||||
? discordAvatarUrl({ id, avatar: avatarHash })
|
||||
: discordProfile
|
||||
? discordAvatarUrl(discordProfile)
|
||||
: discordAvatarUrl({ id, avatar: null }),
|
||||
role: access.role,
|
||||
isOwner: access.isOwner,
|
||||
permissions: access.permissions,
|
||||
};
|
||||
}
|
||||
|
||||
function listUsers(oauth) {
|
||||
bootstrapFromOAuth(oauth);
|
||||
const store = readStore();
|
||||
const yamlOwners = getOwnerIds(oauth);
|
||||
|
||||
return Object.values(store.users).map((entry) => {
|
||||
const access = resolvePermissions(entry, oauth, entry.id) || {
|
||||
permissions: permissionsFromRole("viewer"),
|
||||
role: entry.role,
|
||||
isOwner: false,
|
||||
};
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
username: entry.username,
|
||||
globalName: entry.globalName,
|
||||
avatar: entry.avatar
|
||||
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
|
||||
: discordAvatarUrl({ id: entry.id, avatar: null }),
|
||||
role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role,
|
||||
permissions:
|
||||
entry.role === "custom"
|
||||
? normalizePermissions(entry.permissions)
|
||||
: access.permissions,
|
||||
enabled: entry.enabled !== false,
|
||||
isOwner: yamlOwners.has(String(entry.id)),
|
||||
yamlOwner: yamlOwners.has(String(entry.id)),
|
||||
createdAt: entry.createdAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addUser(payload, actorId, oauth) {
|
||||
const id = String(payload.id || "").trim();
|
||||
if (!/^\d{17,20}$/.test(id)) {
|
||||
throw new Error("Invalid Discord user ID");
|
||||
}
|
||||
|
||||
const role = payload.role || "viewer";
|
||||
if (!ROLES.includes(role)) throw new Error("Invalid role");
|
||||
if (role === "owner" && !isYamlOwner(actorId, oauth)) {
|
||||
throw new Error("Only api.yml owners can assign the owner role");
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
if (store.users[id]) throw new Error("User already exists");
|
||||
|
||||
store.users[id] = {
|
||||
id,
|
||||
username: payload.username || null,
|
||||
globalName: payload.globalName || null,
|
||||
avatar: null,
|
||||
role,
|
||||
permissions:
|
||||
role === "custom" ? normalizePermissions(payload.permissions) : null,
|
||||
enabled: payload.enabled !== false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: String(actorId),
|
||||
};
|
||||
|
||||
writeStore(store);
|
||||
return listUsers(oauth).find((u) => u.id === id);
|
||||
}
|
||||
|
||||
function updateUser(id, payload, actorId, oauth) {
|
||||
const store = readStore();
|
||||
const entry = store.users[String(id)];
|
||||
if (!entry) throw new Error("User not found");
|
||||
|
||||
if (isYamlOwner(id, oauth) && payload.enabled === false) {
|
||||
throw new Error("Cannot disable an owner defined in api.yml");
|
||||
}
|
||||
|
||||
if (payload.role) {
|
||||
if (!ROLES.includes(payload.role)) throw new Error("Invalid role");
|
||||
if (payload.role === "owner" && !isYamlOwner(actorId, oauth)) {
|
||||
throw new Error("Only api.yml owners can assign the owner role");
|
||||
}
|
||||
if (isYamlOwner(id, oauth) && payload.role !== "owner") {
|
||||
throw new Error("Cannot change role of an api.yml owner");
|
||||
}
|
||||
entry.role = payload.role;
|
||||
}
|
||||
|
||||
if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled);
|
||||
if (payload.username !== undefined) entry.username = payload.username;
|
||||
if (payload.globalName !== undefined) entry.globalName = payload.globalName;
|
||||
|
||||
if (payload.permissions !== undefined) {
|
||||
entry.permissions = normalizePermissions(payload.permissions);
|
||||
if (entry.role !== "custom") entry.role = "custom";
|
||||
}
|
||||
|
||||
if (entry.role === "custom" && payload.permissions === undefined && payload.role !== "custom") {
|
||||
entry.permissions = null;
|
||||
}
|
||||
|
||||
entry.updatedAt = Date.now();
|
||||
writeStore(store);
|
||||
return listUsers(oauth).find((u) => u.id === String(id));
|
||||
}
|
||||
|
||||
function removeUser(id, oauth) {
|
||||
if (isYamlOwner(id, oauth)) {
|
||||
throw new Error("Remove this user from OwnerUserIds in api.yml instead");
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
if (!store.users[String(id)]) throw new Error("User not found");
|
||||
delete store.users[String(id)];
|
||||
writeStore(store);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CONFIG_FILES,
|
||||
STORE_PATH,
|
||||
getOwnerIds,
|
||||
isYamlOwner,
|
||||
bootstrapFromOAuth,
|
||||
syncDiscordProfile,
|
||||
getAccess,
|
||||
canLogin,
|
||||
formatPublicUser,
|
||||
listUsers,
|
||||
addUser,
|
||||
updateUser,
|
||||
removeUser,
|
||||
hasPermission,
|
||||
canAccessConfig,
|
||||
ROLES,
|
||||
};
|
||||
Reference in new issue
Block a user