Commit 88310c2e authored by nanahira's avatar nanahira

refa auth and tournament to ts

parent fb166273
/*
Main script of new dashboard account system.
The account list file is stored at `./config/admin_user.json`. The users are stored at `users`.
The key is the username. The `permissions` field could be a string, using a permission set from the example, or an object, to define a specific set of permissions.
eg. An account for a judge could be as follows, to use the default permission of judges,
"username": {
"password": "123456",
"enabled": true,
"permissions": "judge"
},
or as follows, to use a specific set of permissions.
"username": {
"password": "123456",
"enabled": true,
"permissions": {
"get_rooms": true,
"duel_log": true,
"download_replay": true,
"deck_dashboard_read": true,
"deck_dashboard_write": true,
"shout": true,
"kick_user": true,
"start_death": true
}
},
*/
import fs from "fs";
import { sync as loadJSON } from "load-json-file";
import loadJSONPromise from "load-json-file";
import moment from "moment";
import bunyan from "bunyan";
import util from "util";
moment.updateLocale("zh-cn", {
relativeTime: {
future: "%s内",
past: "%s前",
s: "%d秒",
m: "1分钟",
mm: "%d分钟",
h: "1小时",
hh: "%d小时",
d: "1天",
dd: "%d天",
M: "1个月",
MM: "%d个月",
y: "1年",
yy: "%d年",
},
});
const log = bunyan.createLogger({ name: "auth" });
if (!fs.existsSync("./logs")) {
fs.mkdirSync("./logs");
}
type PermissionSet = Record<string, boolean>;
type UserPermissions = string | PermissionSet;
interface UserEntry {
password: string;
enabled: boolean;
permissions: UserPermissions;
[key: string]: any;
}
interface UsersFile {
file?: string;
permission_examples: Record<string, PermissionSet>;
users: Record<string, UserEntry>;
}
const add_log = async function (message: string): Promise<boolean> {
const mt = moment();
log.info(message);
const text = mt.format("YYYY-MM-DD HH:mm:ss") + " --> " + message + "\n";
let res = false;
try {
await fs.promises.appendFile(`./logs/${mt.format("YYYY-MM-DD")}.log`, text);
res = true;
} catch {
res = false;
}
return res;
};
const default_data = loadJSON("./data/default_data.json") as {
users: UsersFile;
};
const setting_save = async function (settings: UsersFile): Promise<void> {
try {
await fs.promises.writeFile(settings.file as string, JSON.stringify(settings, null, 2));
} catch (e) {
add_log("save fail");
}
};
let users: UsersFile;
try {
users = loadJSON("./config/admin_user.json") as UsersFile;
} catch {
users = default_data.users;
setting_save(users);
}
const save = async function (): Promise<void> {
await setting_save(users);
};
const reload = async function (): Promise<void> {
const user_backup = users;
try {
users = (await loadJSONPromise("./config/admin_user.json")) as UsersFile;
} catch {
users = user_backup;
await add_log("Invalid user data JSON");
}
};
const check_permission = async function (
user: UserEntry,
permission_required: string
): Promise<boolean> {
const _permission = user.permissions;
let permission: PermissionSet | undefined;
if (typeof _permission !== "object") {
permission = users.permission_examples[_permission];
} else {
permission = _permission;
}
if (!permission) {
await add_log("Permision not set:" + String(_permission));
return false;
}
return Boolean(permission[permission_required]);
};
export const auth = async function (
name: string,
pass: string,
permission_required: string,
action = "unknown",
no_log?: boolean
): Promise<boolean> {
await reload();
const user = users.users[name];
if (!user) {
await add_log(
"Unknown user login. User: " +
name +
", Permission needed: " +
permission_required +
", Action: " +
action
);
return false;
}
if (user.password !== pass) {
await add_log(
"Unauthorized user login. User: " +
name +
", Permission needed: " +
permission_required +
", Action: " +
action
);
return false;
}
if (!user.enabled) {
await add_log(
"Disabled user login. User: " +
name +
", Permission needed: " +
permission_required +
", Action: " +
action
);
return false;
}
if (!(await check_permission(user, permission_required))) {
await add_log(
"Permission denied. User: " +
name +
", Permission needed: " +
permission_required +
", Action: " +
action
);
return false;
}
if (!no_log) {
await add_log(
"Operation success. User: " +
name +
", Permission needed: " +
permission_required +
", Action: " +
action
);
}
return true;
};
export const add_user = async function (
name: string,
pass: string,
enabled: boolean,
permissions: UserPermissions
): Promise<boolean> {
await reload();
if (users.users[name]) {
return false;
}
users.users[name] = {
password: pass,
enabled: enabled,
permissions: permissions,
};
await save();
return true;
};
export const delete_user = async function (name: string): Promise<void> {
await reload();
if (!users.users[name]) {
return;
}
delete users.users[name];
await save();
};
export const update_user = async function (
name: string,
key: string,
value: unknown
): Promise<void> {
await reload();
if (!users.users[name]) {
return;
}
users.users[name][key] = value;
await save();
};
/*
ygopro-tournament.ts
ygopro tournament util
Author: mercury233
License: MIT
不带参数运行时,会建立一个服务器,调用API执行对应操作
*/
import * as http from "http";
import * as https from "https";
import * as fs from "fs";
import * as url from "url";
import axios from "axios";
import * as formidable from "formidable";
import { sync as loadJSON } from "load-json-file";
import { Challonge } from "./challonge";
import * as asyncLib from "async";
import YGOProDeckEncode from "ygopro-deck-encode";
import * as auth from "./ygopro-auth";
import _ from "underscore";
const settings = loadJSON("./config/config.json") as any;
const config = settings.modules.tournament_mode as any;
const challonge_config = settings.modules.challonge as any;
const challonge = new Challonge(challonge_config);
const ssl_config = settings.modules.http.ssl as any;
//http长连接
let responder: http.ServerResponse | null;
config.wallpapers = [""];
axios
.get("http://www.bing.com/HPImageArchive.aspx", {
params: {
format: "js",
idx: 0,
n: 8,
mkt: "zh-CN",
},
})
.then((response) => {
const body = response.data;
if (typeof body !== "object" || !body.images) {
console.log("wallpapers bad json", body);
} else if (!body) {
console.log("wallpapers error", null, response);
} else {
config.wallpapers = [];
for (const i in body.images) {
const wallpaper = body.images[i];
const img = {
url: "http://s.cn.bing.net" + wallpaper.urlbase + "_768x1366.jpg",
desc: wallpaper.copyright,
};
config.wallpapers.push(img);
}
}
})
.catch((error) => {
console.log("wallpapers error", error, error?.response);
});
//输出反馈信息,如有http长连接则输出到http,否则输出到控制台
const sendResponse = function (text: string) {
text = "" + text;
if (responder) {
text = text.replace(/\n/g, "<br>");
responder.write("data: " + text + "\n\n");
} else {
console.log(text);
}
};
//读取指定卡组
const readDeck = async function (deck_name: string, deck_full_path: string) {
const deck_text = await fs.promises.readFile(deck_full_path, { encoding: "utf-8" });
const deck = YGOProDeckEncode.fromYdkString(deck_text);
deck.name = deck_name;
return deck;
};
//读取指定文件夹中所有卡组
const getDecks = function (callback: (err: Error | null, decks: any[]) => void) {
const decks: any[] = [];
asyncLib.auto(
{
readDir: (done: (err: NodeJS.ErrnoException | null, files?: string[]) => void) => {
fs.readdir(config.deck_path, done);
},
handleDecks: [
"readDir",
(results: any, done: (err?: Error | null) => void) => {
const decks_list = results.readDir as string[];
asyncLib.each(
decks_list,
async (deck_name: string) => {
if (deck_name.endsWith(".ydk")) {
const deck = await readDeck(deck_name, config.deck_path + deck_name);
decks.push(deck);
}
},
done
);
},
],
},
(err: Error | null) => {
callback(err, decks);
}
);
};
const delDeck = function (deck_name: string, callback: (err?: NodeJS.ErrnoException | null) => void) {
if (deck_name.startsWith("../") || deck_name.match(/\/\.\.\//)) {
//security issue
callback(new Error("Invalid deck"));
}
fs.unlink(config.deck_path + deck_name, callback);
};
const clearDecks = function (callback: (err?: Error | null) => void) {
asyncLib.auto(
{
deckList: (done: (err: NodeJS.ErrnoException | null, files?: string[]) => void) => {
fs.readdir(config.deck_path, done);
},
removeAll: [
"deckList",
(results: any, done: (err?: Error | null) => void) => {
const decks_list = results.deckList as string[];
asyncLib.each(decks_list, delDeck as any, done);
},
],
},
callback
);
};
const UploadToChallonge = async function () {
if (!challonge_config.enabled) {
sendResponse("未开启Challonge模式。");
return false;
}
sendResponse("开始读取玩家列表。");
const decks_list = fs.readdirSync(config.deck_path);
const player_list: Array<{ name: string; deckbuf: string }> = [];
for (const k in decks_list) {
const deck_name = decks_list[k];
if (deck_name.endsWith(".ydk")) {
player_list.push({
name: deck_name.slice(0, deck_name.length - 4),
deckbuf: Buffer.from(
YGOProDeckEncode.fromYdkString(
await fs.promises.readFile(config.deck_path + deck_name, { encoding: "utf-8" })
).toUpdateDeckPayload()
).toString("base64"),
});
}
}
if (!player_list.length) {
sendResponse("玩家列表为空。");
return false;
}
sendResponse("读取玩家列表完毕,共有" + player_list.length + "名玩家。");
try {
sendResponse("开始清空 Challonge 玩家列表。");
await challonge.clearParticipants();
sendResponse("开始上传玩家列表至 Challonge。");
for (const chunk of _.chunk(player_list, 10)) {
sendResponse(`开始上传玩家 ${chunk.map((c) => c.name).join(", ")} 至 Challonge。`);
await challonge.uploadParticipants(chunk);
}
sendResponse("玩家列表上传完成。");
} catch (e: any) {
sendResponse("Challonge 上传失败:" + e.message);
}
return true;
};
const receiveDecks = function (
files: any,
callback: (err: Error | null, result: Array<{ file: string; status: string }>) => void
) {
const result: Array<{ file: string; status: string }> = [];
asyncLib.eachSeries(
files,
async (file: any) => {
if (file.name.endsWith(".ydk")) {
const deck = await readDeck(file.name, file.path);
if (deck.main.length >= 40) {
fs.createReadStream(file.path).pipe(fs.createWriteStream(config.deck_path + file.name));
result.push({
file: file.name,
status: "OK",
});
} else {
result.push({
file: file.name,
status: "卡组不合格",
});
}
} else {
result.push({
file: file.name,
status: "不是卡组文件",
});
}
},
(err: Error | null) => {
callback(err, result);
}
);
};
//建立一个http服务器,接收API操作
async function requestListener(req: http.IncomingMessage, res: http.ServerResponse) {
const u = url.parse(req.url || "", true);
// Allow all CORS + PNA (Private Network Access) requests.
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Private-Network", "true");
res.setHeader("Vary", "Origin, Access-Control-Request-Headers, Access-Control-Request-Method");
if ((req.method || "").toLowerCase() === "options") {
const requestHeaders = req.headers["access-control-request-headers"];
res.writeHead(204, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": Array.isArray(requestHeaders)
? requestHeaders.join(", ")
: requestHeaders || "*",
"Access-Control-Allow-Private-Network": "true",
"Access-Control-Max-Age": "86400",
});
res.end();
return;
}
/*if (u.query.password !== config.password) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}*/
if (u.pathname === "/api/upload_decks" && (req.method || "").toLowerCase() == "post") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_write", "upload_deck"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
const form = new (formidable as any).IncomingForm();
form.parse(req, function (err: Error | null, fields: any, files: any) {
receiveDecks(files, (err, result) => {
if (err) {
console.error(`Upload error: ${err}`);
res.writeHead(500, {
"Access-Control-Allow-origin": "*",
"content-type": "text/plain",
});
res.end(JSON.stringify({ error: err.toString() }));
return;
}
res.writeHead(200, {
"Access-Control-Allow-origin": "*",
"content-type": "text/plain",
});
res.end(JSON.stringify(result));
});
});
} else if (u.pathname === "/api/msg") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_read", "login_deck_dashboard"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
res.writeHead(200, {
"Access-Control-Allow-origin": "*",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.on("close", function () {
responder = null;
});
responder = res;
sendResponse("已连接。");
} else if (u.pathname === "/api/get_bg") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_read", "login_deck_dashboard"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
res.writeHead(200);
res.end(
u.query.callback +
"(" +
JSON.stringify(config.wallpapers[Math.floor(Math.random() * config.wallpapers.length)]) +
");"
);
} else if (u.pathname === "/api/get_decks") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_read", "get_decks"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
getDecks((err, decks) => {
if (err) {
res.writeHead(500);
res.end(u.query.callback + "(" + err.toString() + ");");
} else {
res.writeHead(200);
res.end(u.query.callback + "(" + JSON.stringify(decks) + ");");
}
});
} else if (u.pathname === "/api/del_deck") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_write", "delete_deck"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
res.writeHead(200);
delDeck(u.query.msg as string, (err) => {
let result;
if (err) {
result = "删除卡组 " + u.query.msg + "失败: " + err.toString();
} else {
result = "删除卡组 " + u.query.msg + "成功。";
}
res.writeHead(200);
res.end(u.query.callback + '("' + result + '");');
});
} else if (u.pathname === "/api/clear_decks") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_write", "clear_decks"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
clearDecks((err) => {
let result;
if (err) {
result = "删除全部卡组失败。" + err.toString();
} else {
result = "删除全部卡组成功。";
}
res.writeHead(200);
res.end(u.query.callback + '("' + result + '");');
});
} else if (u.pathname === "/api/upload_to_challonge") {
if (!(await auth.auth(u.query.username as string, u.query.password as string, "deck_dashboard_write", "upload_to_challonge"))) {
res.writeHead(403);
res.end("Auth Failed.");
return;
}
res.writeHead(200);
await UploadToChallonge();
res.end(u.query.callback + '("操作完成。");');
} else {
res.writeHead(400);
res.end("400");
}
}
if (ssl_config.enabled) {
const ssl_cert = fs.readFileSync(ssl_config.cert);
const ssl_key = fs.readFileSync(ssl_config.key);
const options = {
cert: ssl_cert,
key: ssl_key,
};
https.createServer(options, requestListener).listen(config.port);
} else {
http.createServer(requestListener).listen(config.port);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment