implements web verification
This commit is contained in:
parent
a7799eb3b1
commit
440e8bf324
9 changed files with 303 additions and 28 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
/secrets.json
|
/conf.json
|
||||||
/rules.json
|
/rules.json
|
||||||
/local
|
/local
|
||||||
|
|
29
deno.lock
generated
29
deno.lock
generated
|
@ -157,9 +157,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirects": {
|
"redirects": {
|
||||||
|
"https://deno.land/x/denomailer/mod.ts": "https://deno.land/x/denomailer@1.6.0/mod.ts",
|
||||||
|
"https://deno.land/x/dotenv/load.ts": "https://deno.land/x/dotenv@v3.2.2/load.ts",
|
||||||
|
"https://deno.land/x/smtp/mod.ts": "https://deno.land/x/smtp@v0.7.0/mod.ts",
|
||||||
"https://deno.land/x/zod/mod.ts": "https://deno.land/x/zod@v3.22.4/mod.ts"
|
"https://deno.land/x/zod/mod.ts": "https://deno.land/x/zod@v3.22.4/mod.ts"
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
|
"https://deno.land/std@0.173.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1",
|
||||||
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
|
"https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
|
||||||
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
|
"https://deno.land/std@0.213.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
|
||||||
"https://deno.land/std@0.213.0/bytes/concat.ts": "9cac3b4376afbef98ff03588eb3cf948e0d1eb6c27cfe81a7651ab6dd3adc54a",
|
"https://deno.land/std@0.213.0/bytes/concat.ts": "9cac3b4376afbef98ff03588eb3cf948e0d1eb6c27cfe81a7651ab6dd3adc54a",
|
||||||
|
@ -257,6 +261,27 @@
|
||||||
"https://deno.land/std@0.213.0/uuid/v3.ts": "aff081baee55498ed5804d006735a77b252ac1645e3b418058807218371de577",
|
"https://deno.land/std@0.213.0/uuid/v3.ts": "aff081baee55498ed5804d006735a77b252ac1645e3b418058807218371de577",
|
||||||
"https://deno.land/std@0.213.0/uuid/v4.ts": "8a9c60c887446651be5d50b468a3d702b87bb821fc35f0edcb5515c3bc07b256",
|
"https://deno.land/std@0.213.0/uuid/v4.ts": "8a9c60c887446651be5d50b468a3d702b87bb821fc35f0edcb5515c3bc07b256",
|
||||||
"https://deno.land/std@0.213.0/uuid/v5.ts": "f6771dc89e89f26e74a9b51d25d6b711c27d2ddf3a3650312dd46e7edfe2491e",
|
"https://deno.land/std@0.213.0/uuid/v5.ts": "f6771dc89e89f26e74a9b51d25d6b711c27d2ddf3a3650312dd46e7edfe2491e",
|
||||||
|
"https://deno.land/std@0.81.0/_util/assert.ts": "e1f76e77c5ccb5a8e0dbbbe6cce3a56d2556c8cb5a9a8802fc9565af72462149",
|
||||||
|
"https://deno.land/std@0.81.0/bytes/mod.ts": "e4f91c6473fe13e3cf1a23649137f87f49135c10bc08fc0f83382a0fb0b03744",
|
||||||
|
"https://deno.land/std@0.81.0/encoding/utf8.ts": "1b7e77db9a12363c67872f8a208886ca1329f160c1ca9133b13d2ed399688b99",
|
||||||
|
"https://deno.land/std@0.81.0/io/bufio.ts": "3cbbe1f761c1c636d1e7128ed4e7fdca6bf21d9199aa3cae71e69972a6ae8f93",
|
||||||
|
"https://deno.land/std@0.81.0/textproto/mod.ts": "4c378eda3cb6216608bb4c3a34201761c65f6980c4669455ca224c330cd5b790",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/basic/QUE.ts": "5af1dfcc5814bf4542f098908ac1fdd8a1a1c2b1597138a121c95eaa791315d0",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/basic/client.ts": "462e4db45ae218647812ceae720d55ea33e0e928f9138fee9da5913cbb1e20f9",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/basic/connection.ts": "68de68d7551d8303629905c2b7581cb09b45646e530ce93a5786ca1aba61055c",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/basic/transforms.ts": "e630a23d24e9b397e231ae8796c0a0080770ac6f5ab9bffc105d3717706e62c9",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/mod.ts": "8ec4c25d9586f83f8629768311a077eaf03b1490dcb872030ba2e27fadb674d8",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/pool.ts": "0466e69ca8959aa85501cc6b30d7f5fd8e43b0a6ac88ecc60dab71081e801bae",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/client/worker/worker.ts": "a4c3a3e2e1fde0967817ece7c345a565eb44a7312acf8d46ce620d4ff4443b31",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/client.ts": "302f5c18fbb5531b5615613084b86d44120acc210e072f4135e431fa27fc4526",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/mail/attachments.ts": "1f357bddc9d5e813c3f647498db81a165a1a8a7163116c58dc10cf01427fd81e",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/mail/content.ts": "3925d4c3baaabed4e08933159d34b1450e6426b35a5bc323a0666780bef20192",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/mail/email.ts": "bb0ca104bf9cb54af6613a04b3f8cb05290f4fb012e7113f1d050cab10226a7c",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/mail/encoding.ts": "0bc5983ada3b902333925cdca225f8ea5e28fffbc2f1bd2b0ccb9a423f6f7fcc",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/mail/headers.ts": "ce94874beb5a1a7248b5b91bf1ae3b3aed2d4c0541f3f448f2bbfad6c8f570ee",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/config/mail/mod.ts": "a7fafa3386a45a585d7983d816b09ddc28b2f2b84097a614f1e38685b1f62868",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/deps.ts": "12bef188bb2a490fedc82ac1889f3d438e8a15887c423b045fee532b31a43102",
|
||||||
|
"https://deno.land/x/denomailer@1.6.0/mod.ts": "71a197dff098194ab53691abd3c9d22a276ef04e1382eb85f5632dbcb5a83bf3",
|
||||||
"https://deno.land/x/discordeno@18.0.1/bot.ts": "b6c4f1c966f1a968186921619b6e5ebfec7c5eb0dc2e49a66d2c86b37cb2acc7",
|
"https://deno.land/x/discordeno@18.0.1/bot.ts": "b6c4f1c966f1a968186921619b6e5ebfec7c5eb0dc2e49a66d2c86b37cb2acc7",
|
||||||
"https://deno.land/x/discordeno@18.0.1/gateway/manager/calculateTotalShards.ts": "2d2ebe860861d58524416446426d78e5b881c17b3a565ea4822c67f5534214bc",
|
"https://deno.land/x/discordeno@18.0.1/gateway/manager/calculateTotalShards.ts": "2d2ebe860861d58524416446426d78e5b881c17b3a565ea4822c67f5534214bc",
|
||||||
"https://deno.land/x/discordeno@18.0.1/gateway/manager/calculateWorkerId.ts": "44c46f2977104a5f92cc21cf31d6b2bc5dcfcefba23495cd619dbdf074a00af1",
|
"https://deno.land/x/discordeno@18.0.1/gateway/manager/calculateWorkerId.ts": "44c46f2977104a5f92cc21cf31d6b2bc5dcfcefba23495cd619dbdf074a00af1",
|
||||||
|
@ -646,6 +671,10 @@
|
||||||
"https://deno.land/x/discordeno@18.0.1/util/utils.ts": "b16797ea1918af635f0c04c345a7c9b57c078310ac18d0c86936ec8abfaeddeb",
|
"https://deno.land/x/discordeno@18.0.1/util/utils.ts": "b16797ea1918af635f0c04c345a7c9b57c078310ac18d0c86936ec8abfaeddeb",
|
||||||
"https://deno.land/x/discordeno@18.0.1/util/validateLength.ts": "7c610911d72082f9cfe2c455737cd37d8ce8f323483f0ef65fdfea6a993984b5",
|
"https://deno.land/x/discordeno@18.0.1/util/validateLength.ts": "7c610911d72082f9cfe2c455737cd37d8ce8f323483f0ef65fdfea6a993984b5",
|
||||||
"https://deno.land/x/discordeno@18.0.1/util/verifySignature.ts": "8ba1c3d2698f347b4f32a76bd33edeb67ee9d23c34f419a797c393926786bb97",
|
"https://deno.land/x/discordeno@18.0.1/util/verifySignature.ts": "8ba1c3d2698f347b4f32a76bd33edeb67ee9d23c34f419a797c393926786bb97",
|
||||||
|
"https://deno.land/x/smtp@v0.7.0/code.ts": "f388fae4995b4d35d99fb6b8bfded522f5a3e7e7d63babdf318a059d6db43baf",
|
||||||
|
"https://deno.land/x/smtp@v0.7.0/deps.ts": "5e2a437e3ae35f0e83719fd2e707858dcb750c1111ff5bebc729522a1380b53d",
|
||||||
|
"https://deno.land/x/smtp@v0.7.0/mod.ts": "9b0d8fbdacc184d1af10f727980e51486e0ddf9d2ec7227c8dfce90db5bfbcf5",
|
||||||
|
"https://deno.land/x/smtp@v0.7.0/smtp.ts": "47c72a99925ad07f3174037f9325dbb8b703dc1177277b9161dc6209c7fa4f90",
|
||||||
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
||||||
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
||||||
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
||||||
|
|
10
src/bot.ts
10
src/bot.ts
|
@ -70,6 +70,15 @@ export class EpitlsBot {
|
||||||
return this.bot.user?.displayName;
|
return this.bot.user?.displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async get_username(user_id: string) {
|
||||||
|
try {
|
||||||
|
const user = await this.bot.users.fetch(user_id);
|
||||||
|
return user.displayName;
|
||||||
|
} catch (_) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async register_commands() {
|
private async register_commands() {
|
||||||
const cmd = new SlashCommandBuilder()
|
const cmd = new SlashCommandBuilder()
|
||||||
.setName("associate")
|
.setName("associate")
|
||||||
|
@ -103,6 +112,7 @@ export class EpitlsBot {
|
||||||
|
|
||||||
this.assoc_channel.send({ cri_login, discord_user_id, callback: resolver });
|
this.assoc_channel.send({ cri_login, discord_user_id, callback: resolver });
|
||||||
await interaction.reply(message_command_response_sending_email(email));
|
await interaction.reply(message_command_response_sending_email(email));
|
||||||
|
log(`Started verification for discord id '${discord_user_id}' with cri login '${cri_login}'.`);
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
if (result === true) interaction.editReply(message_command_response_success());
|
if (result === true) interaction.editReply(message_command_response_success());
|
||||||
else interaction.editReply(message_command_response_error(result));
|
else interaction.editReply(message_command_response_error(result));
|
||||||
|
|
68
src/email.ts
68
src/email.ts
|
@ -1,4 +1,6 @@
|
||||||
import { SmtpClient } from "https://deno.land/x/smtp@v0.7.0/mod.ts";
|
import { ClientOptions, SMTPClient } from "https://deno.land/x/denomailer@1.6.0/mod.ts";
|
||||||
|
import { log_from } from "./utils.ts";
|
||||||
|
const log = (...args: unknown[]) => log_from(import.meta.url, ...args);
|
||||||
|
|
||||||
export type EmailerConfig = {
|
export type EmailerConfig = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
@ -11,21 +13,61 @@ export type EmailerConfig = {
|
||||||
* Wraps emailing process.
|
* Wraps emailing process.
|
||||||
*/
|
*/
|
||||||
export class Emailer {
|
export class Emailer {
|
||||||
private client;
|
private sender_address;
|
||||||
private config;
|
private client_options;
|
||||||
private sender_email;
|
|
||||||
|
|
||||||
public constructor(config: EmailerConfig, sender_email: string) {
|
public constructor(config: EmailerConfig) {
|
||||||
this.config = config;
|
const client_options: ClientOptions = {
|
||||||
this.sender_email = sender_email;
|
connection: {
|
||||||
this.client = new SmtpClient();
|
hostname: config.hostname,
|
||||||
|
auth: {
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
},
|
||||||
|
port: config.port,
|
||||||
|
tls: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.client_options = client_options;
|
||||||
|
this.sender_address = config.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(to_email: string, subject: string, content: string) {
|
public async send_confirmation_mail(discord_username: string, cri_email: string, link: string) {
|
||||||
const from = this.sender_email;
|
await this.send(cri_email, CONFIRMATION_EMAIL_SUBJECT, confirmation_email_body(discord_username, link));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async send(to_email: string, subject: string, content: string) {
|
||||||
|
const client = new SMTPClient(this.client_options);
|
||||||
|
const from = this.sender_address;
|
||||||
const to = to_email;
|
const to = to_email;
|
||||||
await this.client.connectTLS(this.config);
|
await client.send({ from, to, subject, html: content });
|
||||||
await this.client.send({ from, to, subject, content });
|
await client.close();
|
||||||
await this.client.close();
|
log(`Sent an email to '${to_email}'.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONFIRMATION_EMAIL_SUBJECT = "Confirmation d'association à un compte discord";
|
||||||
|
function confirmation_email_body(discord_username: string, link: string) {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head> <meta charset="UTF-8"> </head>
|
||||||
|
<body>
|
||||||
|
<pre>
|
||||||
|
Bonjour,
|
||||||
|
Ceci est un message automatique de confirmation pour l'association
|
||||||
|
du compte discord '${discord_username}' à cet email.
|
||||||
|
|
||||||
|
Pour terminer l'association, veuillez suivre sur le lien suivant :
|
||||||
|
<a href="${link}">${link}</a>
|
||||||
|
|
||||||
|
Si vous n'êtes pas à l'origine de cette demande, veuillez ignorer ce message.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
Je suis un robot et cette action à été effectuée automatiquement.
|
||||||
|
Vous pouvez contacter le développeur de se service à l'email matthieu at imagevo dot fr.
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
45
src/main.ts
45
src/main.ts
|
@ -1,24 +1,35 @@
|
||||||
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --unstable-kv
|
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --unstable-kv
|
||||||
|
|
||||||
import { log_from, read_secrets, root_path, SimpleResult, wait } from "./utils.ts";
|
import { log_from, read_conf, root_path, SimpleResult, wait } from "./utils.ts";
|
||||||
import { State } from "./state.ts";
|
import { State } from "./state.ts";
|
||||||
import { RuleSet } from "./rules.ts";
|
import { RuleSet } from "./rules.ts";
|
||||||
import { EpitlsBot } from "./bot.ts";
|
import { EpitlsBot } from "./bot.ts";
|
||||||
import { CriApi } from "./cri.ts";
|
import { CriApi } from "./cri.ts";
|
||||||
|
import { Emailer } from "./email.ts";
|
||||||
|
import { WebVerifier } from "./verifier.ts";
|
||||||
const log = (...args: unknown[]) => log_from(import.meta.url, ...args);
|
const log = (...args: unknown[]) => log_from(import.meta.url, ...args);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const secrets = await read_secrets(root_path() + "/secrets.json");
|
const conf = await read_conf(root_path() + "/conf.json");
|
||||||
|
|
||||||
const rules = await RuleSet.from_file(root_path() + "/rules.json");
|
const rules = await RuleSet.from_file(root_path() + "/rules.json");
|
||||||
log("Loaded rules for", rules.size(), "roles.");
|
log("Loaded rules for", rules.size(), "roles.");
|
||||||
|
|
||||||
const state = await State.from_dir(root_path() + "/local");
|
const state = await State.from_dir(root_path() + "/local");
|
||||||
log("Loaded state with", await state.users_count(), "users.");
|
log("Loaded state with", await state.users_count(), "users.");
|
||||||
const bot = new EpitlsBot(secrets.discord_bot_token);
|
|
||||||
|
const bot = new EpitlsBot(conf.discord.bot_token);
|
||||||
await bot.start();
|
await bot.start();
|
||||||
log(`Started bot '${bot.bot_name()}' .`);
|
log(`Started bot '${bot.bot_name()}' .`);
|
||||||
const cri_api = new CriApi(secrets.cri_token);
|
|
||||||
|
|
||||||
const service = new Service(state, bot, cri_api, rules);
|
const verifier = new WebVerifier(conf.verif_http);
|
||||||
|
await verifier.start();
|
||||||
|
log(`Started web verifier at '${verifier.url()}' .`);
|
||||||
|
|
||||||
|
const cri_api = new CriApi(conf.cri.cri_token);
|
||||||
|
const mailer = new Emailer(conf.email_smtp);
|
||||||
|
|
||||||
|
const service = new Service(state, bot, cri_api, rules, mailer, verifier);
|
||||||
await service.serve();
|
await service.serve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,25 +41,38 @@ class Service {
|
||||||
bot;
|
bot;
|
||||||
cri_api;
|
cri_api;
|
||||||
rules;
|
rules;
|
||||||
|
emailer;
|
||||||
|
verifier;
|
||||||
|
|
||||||
constructor(state: State, bot: EpitlsBot, cri_api: CriApi, rules: RuleSet) {
|
constructor(state: State, bot: EpitlsBot, cri_api: CriApi, rules: RuleSet, emailer: Emailer, verifier: WebVerifier) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
this.cri_api = cri_api;
|
this.cri_api = cri_api;
|
||||||
this.rules = rules;
|
this.rules = rules;
|
||||||
|
this.emailer = emailer;
|
||||||
|
this.verifier = verifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main loops.
|
* Launches main loops.
|
||||||
*/
|
*/
|
||||||
async serve() {
|
async serve() {
|
||||||
await this.update_all_users_roles();
|
await this.update_all_users_roles();
|
||||||
|
|
||||||
|
// for all received associations, trigger the association procedure.
|
||||||
(async () => {
|
(async () => {
|
||||||
for await (const { discord_user_id, cri_login, callback } of this.bot.receive_associations()) {
|
for await (const { discord_user_id, cri_login, callback } of this.bot.receive_associations()) {
|
||||||
this.association_procedure(discord_user_id, cri_login).then(callback);
|
this.association_procedure(discord_user_id, cri_login).then(callback);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// for all links that must be sent, trigger mail sending.
|
||||||
|
(async () => {
|
||||||
|
for await (const { cri_login, discord_id, link } of this.verifier.links_to_send()) {
|
||||||
|
const username = await this.bot.get_username(discord_id);
|
||||||
|
this.emailer.send_confirmation_mail(username ?? "<unknown>", cri_login + "@epita.fr", link);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async update_all_users_roles() {
|
async update_all_users_roles() {
|
||||||
|
@ -61,15 +85,18 @@ class Service {
|
||||||
|
|
||||||
async update_user_roles(cri_login: string, discord_user_id: string) {
|
async update_user_roles(cri_login: string, discord_user_id: string) {
|
||||||
const groups = await this.cri_api.groups_of(cri_login);
|
const groups = await this.cri_api.groups_of(cri_login);
|
||||||
|
// log("found groups", groups);
|
||||||
const roles = groups.map((group) => this.rules.roles_for_group(group)).flat();
|
const roles = groups.map((group) => this.rules.roles_for_group(group)).flat();
|
||||||
|
// log("found setting roles", roles);
|
||||||
for (const { guild_id, role_id } of roles) await this.bot.assign_role(discord_user_id, guild_id, role_id);
|
for (const { guild_id, role_id } of roles) await this.bot.assign_role(discord_user_id, guild_id, role_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async association_procedure(discord_user_id: string, cri_login: string): Promise<SimpleResult> {
|
async association_procedure(discord_user_id: string, cri_login: string): Promise<SimpleResult> {
|
||||||
try {
|
try {
|
||||||
await wait(1000);
|
|
||||||
if (!await this.cri_api.user_exists(cri_login)) return "No such login.";
|
if (!await this.cri_api.user_exists(cri_login)) return "No such login.";
|
||||||
this.state.set_user(discord_user_id, cri_login);
|
const res = await this.verifier.verification(discord_user_id, cri_login);
|
||||||
|
if (res !== true) return res;
|
||||||
|
await this.state.set_user(discord_user_id, cri_login);
|
||||||
await this.update_user_roles(cri_login, discord_user_id);
|
await this.update_user_roles(cri_login, discord_user_id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
12
src/rules.ts
12
src/rules.ts
|
@ -1,5 +1,6 @@
|
||||||
|
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
||||||
|
|
||||||
export type TargetRole = { guild_id: string; role_id: string };
|
export type TargetRole = { guild_id: string; role_id: string };
|
||||||
type SerializedRule = { group_id: string; target_role: TargetRole };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of rules for associating CRI groups with Discord roles.
|
* A set of rules for associating CRI groups with Discord roles.
|
||||||
|
@ -17,7 +18,7 @@ export class RuleSet {
|
||||||
public static async from_file(path: string) {
|
public static async from_file(path: string) {
|
||||||
const result = new RuleSet();
|
const result = new RuleSet();
|
||||||
const file_content = await Deno.readTextFile(path);
|
const file_content = await Deno.readTextFile(path);
|
||||||
const parsed = JSON.parse(file_content) as SerializedRule[];
|
const parsed = parse_rules(file_content);
|
||||||
for (const { group_id, target_role } of parsed) result.append_rule(group_id, target_role);
|
for (const { group_id, target_role } of parsed) result.append_rule(group_id, target_role);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -47,3 +48,10 @@ export class RuleSet {
|
||||||
roles.push(target_role);
|
roles.push(target_role);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parse_rules(content: string) {
|
||||||
|
return z.array(z.object({
|
||||||
|
group_id: z.string(),
|
||||||
|
target_role: z.object({ guild_id: z.string(), role_id: z.string() }),
|
||||||
|
})).parse(JSON.parse(content));
|
||||||
|
}
|
||||||
|
|
43
src/utils.ts
43
src/utils.ts
|
@ -46,6 +46,8 @@ export function split_promise<T>() {
|
||||||
return { promise, resolver };
|
return { promise, resolver };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Resolver<T> = (item: T) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logging function factory.
|
* Logging function factory.
|
||||||
*/
|
*/
|
||||||
|
@ -55,17 +57,54 @@ export function log_from(url: string, ...args: unknown[]) {
|
||||||
console.log(`[${date}][epitls][${file}]`, ...args);
|
console.log(`[${date}][epitls][${file}]`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Conf = Awaited<ReturnType<typeof read_conf>>;
|
||||||
/**
|
/**
|
||||||
* Reads and parse a configuration file containing the current instance' secrets.
|
* Reads and parse a configuration file containing the current instance' secrets.
|
||||||
*/
|
*/
|
||||||
export async function read_secrets(path: string) {
|
export async function read_conf(path: string) {
|
||||||
const content = await Deno.readTextFile(path);
|
const content = await Deno.readTextFile(path);
|
||||||
return z.object({
|
return z.object({
|
||||||
discord_bot_token: z.string(),
|
discord: z.object({
|
||||||
|
bot_token: z.string(),
|
||||||
|
}),
|
||||||
|
cri: z.object({
|
||||||
cri_token: z.string(),
|
cri_token: z.string(),
|
||||||
|
}),
|
||||||
|
email_smtp: z.object({
|
||||||
|
hostname: z.string(),
|
||||||
|
port: z.number(),
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
}),
|
||||||
|
verif_http: z.object({
|
||||||
|
port: z.number(),
|
||||||
|
url_prefix: z.string(),
|
||||||
|
}),
|
||||||
}).parse(JSON.parse(content));
|
}).parse(JSON.parse(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function placeholder_conf() {
|
||||||
|
const result: Conf = {
|
||||||
|
"discord": {
|
||||||
|
"bot_token": "available at 'https://discord.com/developers/applications'",
|
||||||
|
},
|
||||||
|
"cri": {
|
||||||
|
"cri_token": "'username:password' base64-encoded",
|
||||||
|
},
|
||||||
|
"email_smtp": {
|
||||||
|
"hostname": "smtp.gmail.com",
|
||||||
|
"port": 465,
|
||||||
|
"username": "bot@gmail.com",
|
||||||
|
"password": "PWD",
|
||||||
|
},
|
||||||
|
"verif_http": {
|
||||||
|
"port": 80,
|
||||||
|
"url_prefix": "localhost",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return JSON.stringify(result, null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
export type SimpleResult = true | string;
|
export type SimpleResult = true | string;
|
||||||
|
|
||||||
export async function wait(ms: number) {
|
export async function wait(ms: number) {
|
||||||
|
|
115
src/verifier.ts
Normal file
115
src/verifier.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { v1 as uuid } from "https://deno.land/std@0.213.0/uuid/mod.ts";
|
||||||
|
import { channel, log_from, Resolver, SimpleResult, split_promise, wait } from "./utils.ts";
|
||||||
|
const log = (...args: unknown[]) => log_from(import.meta.url, ...args);
|
||||||
|
|
||||||
|
export type WebVerifierConf = {
|
||||||
|
port: number;
|
||||||
|
url_prefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LinkToSend = {
|
||||||
|
discord_id: string;
|
||||||
|
cri_login: string;
|
||||||
|
link: string;
|
||||||
|
};
|
||||||
|
export class WebVerifier {
|
||||||
|
conf;
|
||||||
|
awaiting;
|
||||||
|
link_channel;
|
||||||
|
|
||||||
|
public constructor(conf: WebVerifierConf) {
|
||||||
|
this.conf = conf;
|
||||||
|
this.awaiting = new Map<string, Resolver<SimpleResult>>();
|
||||||
|
this.link_channel = channel<LinkToSend>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
const { promise, resolver } = split_promise<void>();
|
||||||
|
Deno.serve({
|
||||||
|
hostname: "0.0.0.0",
|
||||||
|
port: this.conf.port,
|
||||||
|
onListen: () => resolver(),
|
||||||
|
}, (request) => this.serve(request));
|
||||||
|
return await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verification(discord_id: string, cri_login: string) {
|
||||||
|
const { link, uid } = this.create_verif_link();
|
||||||
|
const { promise, resolver } = split_promise<SimpleResult>();
|
||||||
|
this.awaiting.set(uid, resolver);
|
||||||
|
log(`Created verification link '${link}' for discord id '${discord_id}' to cri login '${cri_login}'.`);
|
||||||
|
this.link_channel.send({ cri_login, discord_id, link });
|
||||||
|
this.start_peremption(uid);
|
||||||
|
return await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async *links_to_send() {
|
||||||
|
while (true) yield await this.link_channel.receive();
|
||||||
|
}
|
||||||
|
|
||||||
|
public url() {
|
||||||
|
return `http://${this.conf.url_prefix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private serve(request: Request): Response {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (!url.pathname.startsWith("/verify/")) return response_failure("Invalid path.");
|
||||||
|
|
||||||
|
const [uid] = url.pathname.slice("/verify/".length).split("/");
|
||||||
|
const resolver = this.awaiting.get(uid);
|
||||||
|
if (resolver === undefined) return response_failure("Invalid verification link.");
|
||||||
|
|
||||||
|
resolver(true);
|
||||||
|
this.awaiting.delete(uid);
|
||||||
|
log(`Verified link ${uid} successfully.`);
|
||||||
|
return response_success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private create_verif_link() {
|
||||||
|
const uid = uuid.generate() as string;
|
||||||
|
const link = `http://${this.conf.url_prefix}/verify/${uid}`;
|
||||||
|
return { uid, link };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async start_peremption(uid: string) {
|
||||||
|
await wait(5 * 60 * 1000); // 5 mins
|
||||||
|
const resolver = this.awaiting.get(uid);
|
||||||
|
if (resolver === undefined) return;
|
||||||
|
resolver("Verification link timeout.");
|
||||||
|
this.awaiting.delete(uid);
|
||||||
|
log(`Link '${uid}' timed out.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function response_failure(message: string) {
|
||||||
|
const body = `<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Epitls • Verification</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<pre>
|
||||||
|
❌ Failure :
|
||||||
|
${message}
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
return new Response(body, { headers: { "Content-Type": "text/html" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function response_success() {
|
||||||
|
const body = `<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Epitls • Verification</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<pre>
|
||||||
|
✅ Verified successfully.
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
return new Response(body, { headers: { "Content-Type": "text/html" } });
|
||||||
|
}
|
5
watch.sh
Executable file
5
watch.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$(realpath "$0")")"
|
||||||
|
|
||||||
|
nodemon -w src -e ts -x ./src/main.ts
|
Loading…
Add table
Add a link
Reference in a new issue