cryptpad/src/worker/modules/badge.ts
2025-06-26 17:24:14 +02:00

143 lines
4.2 KiB
TypeScript

// SPDX-FileCopyrightText: 2025 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import nacl from 'tweetnacl/nacl-fast';
import nThen from 'nthen';
import { Module, ModuleObject, Callback } from '../types'
import * as Util from '../../common/common-util.js';
export interface BadgeModule<T> extends Module<T> {
setCustomize: (data: any) => void
}
let ApiConfig:any = {};
const checkApi = (ctx, ed, cb) => {
const badges = [];
const origin:string = ApiConfig?.httpUnsafeOrigin;
Util.fetchApi(origin, 'config', true, Config => {
const isAdmin = Config?.adminKeys?.includes(ed);
if (isAdmin) { badges.push('admin'); }
const isModerator = Config?.moderatorKeys?.includes(ed);
if (isModerator) { badges.push('moderator'); }
cb(badges);
});
};
const checkPremium = (ctx, ed, cb) => {
ctx.Store.anonRpcMsg('', {
msg: 'IS_PREMIUM',
data: ed
}, obj => {
let premium = Array.isArray(obj) && obj[0];
cb(premium ? ['premium'] : []);
});
};
const listBadges:Callback = (ctx, data, clientId, cb) => {
const badges = [];
const origin:string = ApiConfig?.httpUnsafeOrigin;
const ed = data.edPublic || ctx.store?.proxy?.edPublic;
nThen(waitFor => {
checkApi(ctx, ed, waitFor(list => {
Array.prototype.push.apply(badges, list);
}));
}).nThen(waitFor => {
checkPremium(ctx, ed, waitFor(list => {
Array.prototype.push.apply(badges, list);
}));
}).nThen(() => {
cb(badges);
});
};
const allBadges = {
'admin': checkApi,
'moderator': checkApi,
'premium': checkPremium
};
const checkBadge:Callback = (ctx, data, clientId, cb) => {
const { badge, ed, sig, nid/*, channel*/ } = data;
const publicKey = Util.decodeBase64(ed); // Public key uint8
const signature = Util.decodeBase64(sig); // badge uint8
// Check signature
const msg = nacl.sign.open(signature, publicKey);
if (!msg) { return void cb({verified: false}); }
// Check channel (replay attack)
const msgStr:string = Util.encodeUTF8(msg);
const expected:string = nid; // + channel;
if (msgStr !== expected) { return void cb({verified: false}); }
let f = allBadges[badge];
if (!f) { return void cb({verified: false, error: 'EINVAL'}); }
f(ctx, ed, list => {
cb({
verified: list.includes(badge),
badge: badge
});
});
};
const Badge: BadgeModule<ModuleObject> = {
init: (config, cb, emit) => {
const ctx:any = {
store: config.store,
Store: config.Store,
updateMetadata: config.updateMetadata
};
const _listBadges = (data, cb) => {
listBadges(ctx, data, undefined, cb);
};
// On ready, check if our badge is still valid
const Store = ctx.Store;
Store.onReadyEvt.reg(() => {
const md = Store.getMetadata(void 0, 'drive', () => {});
const myBadge:string = md?.user?.badge || "";
if (!myBadge) { return; }
listBadges(ctx, {}, undefined, all => {
if (!all.includes(myBadge)) {
const profile = ctx?.store?.modules?.profile;
profile?.execCommand(void 0, {
cmd: 'SET',
data: {
key: 'badge',
value: ''
}
}, () => {});
}
});
});
return {
listBadges: _listBadges,
removeClient: () => {},
execCommand: (clientId, obj, cb) => {
const cmd = obj.cmd;
const data = obj.data;
if (cmd === 'LIST_BADGES') {
return void listBadges(ctx, data, clientId, cb);
}
if (cmd === 'CHECK_BADGE') {
return void checkBadge(ctx, data, clientId, cb);
}
cb();
},
}
},
setCustomize: data => {
ApiConfig = data?.ApiConfig;
}
};
export { Badge }