FTStore now holds both halves of the product:
ios/ the swift app — the fork of claration/Feather, unchanged apart
from its new home
web/ FTStore Admin, formerly its own repository. serves the
dashboard at /admin, the ftstore.json feed and IPA downloads
app-repo.json and Images/ stay at the repo root on purpose: the former is
a published AltStore source whose raw.githubusercontent URL is what
installed copies resolve updates through, so moving it would break every
subscriber.
The admin server is renamed throughout: package ftstore-admin, images
ghcr.io/<owner>/ftstore-admin-{app,worker}, postgres role/db ftstore,
session cookie ftstore_session, temp dir /tmp/ftstore, backup type
ftstore-settings, outbound user-agent FTStoreAdmin. backups written under the old
name still import, and compose is pinned to name: ftstore so the
volume prefix does not follow the directory name.
apply-rebrand.sh gains a relocation step: upstream's tree is rooted at
the repo root while ours lives in ios/, so a merge drops Feather's files
next to ios/ and the script folds them in before renaming. it never
touches web/. still idempotent.
release tags are namespaced — v* for the app, web-v* for the server —
so a server release no longer triggers a macOS build or rewrites
app-repo.json. workflows are prefixed ios-/web- and path-filtered.
130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import { PrismaClient } from "@prisma/client";
|
|
import { randomBytes, scryptSync } from "crypto";
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
function hashPassword(password: string): string {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(password, salt, 64).toString("hex");
|
|
return `${salt}:${hash}`;
|
|
}
|
|
|
|
async function main() {
|
|
const [, , command, ...args] = process.argv;
|
|
|
|
switch (command) {
|
|
case "create": {
|
|
const [username, password] = args;
|
|
const isAdmin = args.includes("--admin");
|
|
if (!username || !password) {
|
|
console.error("Usage: manage-users create <username> <password> [--admin]");
|
|
process.exit(1);
|
|
}
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username,
|
|
passwordHash: hashPassword(password),
|
|
role: isAdmin ? "admin" : "user",
|
|
approved: true,
|
|
},
|
|
});
|
|
console.log(`Created user: ${user.username} (role: ${user.role}, approved: true)`);
|
|
break;
|
|
}
|
|
|
|
case "list": {
|
|
const users = await prisma.user.findMany({
|
|
select: { id: true, username: true, role: true, approved: true, createdAt: true },
|
|
orderBy: { createdAt: "asc" },
|
|
});
|
|
if (users.length === 0) {
|
|
console.log("No users found.");
|
|
} else {
|
|
console.table(users.map((u) => ({
|
|
ID: u.id,
|
|
Username: u.username,
|
|
Role: u.role,
|
|
Approved: u.approved ? "Yes" : "No",
|
|
Created: u.createdAt.toISOString(),
|
|
})));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "approve": {
|
|
const [username] = args;
|
|
if (!username) {
|
|
console.error("Usage: manage-users approve <username>");
|
|
process.exit(1);
|
|
}
|
|
await prisma.user.update({
|
|
where: { username },
|
|
data: { approved: true },
|
|
});
|
|
console.log(`Approved user: ${username}`);
|
|
break;
|
|
}
|
|
|
|
case "set-role": {
|
|
const [username, role] = args;
|
|
if (!username || !role || !["admin", "user"].includes(role)) {
|
|
console.error("Usage: manage-users set-role <username> <admin|user>");
|
|
process.exit(1);
|
|
}
|
|
await prisma.user.update({
|
|
where: { username },
|
|
data: { role },
|
|
});
|
|
console.log(`Set role for ${username}: ${role}`);
|
|
break;
|
|
}
|
|
|
|
case "reset-password": {
|
|
const [username, password] = args;
|
|
if (!username || !password) {
|
|
console.error("Usage: manage-users reset-password <username> <password>");
|
|
process.exit(1);
|
|
}
|
|
await prisma.user.update({
|
|
where: { username },
|
|
data: { passwordHash: hashPassword(password) },
|
|
});
|
|
console.log(`Password reset for: ${username}`);
|
|
break;
|
|
}
|
|
|
|
case "delete": {
|
|
const [username] = args;
|
|
if (!username) {
|
|
console.error("Usage: manage-users delete <username>");
|
|
process.exit(1);
|
|
}
|
|
await prisma.user.delete({ where: { username } });
|
|
console.log(`Deleted user: ${username}`);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
console.log(`FTStore Admin User Management CLI
|
|
|
|
Commands:
|
|
create <username> <password> [--admin] Create a new user
|
|
list List all users
|
|
approve <username> Approve a pending user
|
|
set-role <username> <admin|user> Change user role
|
|
reset-password <username> <password> Reset user password
|
|
delete <username> Delete a user
|
|
|
|
Docker usage:
|
|
docker exec -it <container> npx tsx scripts/manage-users.ts <command>`);
|
|
if (command) process.exit(1);
|
|
}
|
|
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|