diff --git a/ajax/verify-unt-email.php b/ajax/verify-unt-email.php new file mode 100644 index 00000000..a1d5da43 --- /dev/null +++ b/ajax/verify-unt-email.php @@ -0,0 +1,195 @@ +query("SELECT + id, + created_on + FROM + discord_verification_tokens + WHERE + user_id = {$auth['id']} + ORDER BY created_on DESC + LIMIT 1"); + if ($q === false) { + error_log("Could not retrieve discord verification token for user {$auth['id']}: {$db->error}"); + echo 'ERROR'; + die(); + } + if ($q->num_rows > 0) { + $r = $q->fetch_assoc(); + /** @noinspection PhpParenthesesCanBeOmittedForNewCallInspection */ + /** @noinspection PhpUnhandledExceptionInspection */ + if ((new DateTime())->sub(new DateInterval('PT' . EMAIL_TOKEN_GENERATION_RATE_LIMIT . 'S')) < new DateTime($r['created_on'])) { + echo 'TOKEN_RATELIMIT'; + die(); + } + $q = $db->query("DELETE FROM + discord_verification_tokens + WHERE + id = {$r['id']}" + ); + if($q === false){ + error_log("Could not delete discord verification token {$r['id']} for user {$auth['id']}: {$db->error}"); + echo 'ERROR'; + die(); + } + } + // generate token + $token = bin2hex(openssl_random_pseudo_bytes(3)); + $q = $db->query("INSERT INTO discord_verification_tokens ( + token, + created_on, + user_id, + unt_email + ) + VALUES ( + '{$token}', + CURRENT_TIMESTAMP(), + {$auth['id']}, + '{$db->real_escape_string($email)}' + )" + ); + if ($q === false) { + error_log("Could not add discord verification token to db for user {$auth['id']}: {$db->error}"); + echo 'ERROR'; + die(); + } + // send email with token + $email_send_status = email( + $email, + "UNT Robotics Discord verification token", + + "
" . + '' . + + '
' . + + '
' . + "

Hey there!

" . + "

Welcome to the team!!

" . + "

Thank you for joining the UNT Robotics Discord server!" . + "

Your verification token is:

" . + "
" . + "
" . + "

{$token}

" . + '
' . + + '
' . + + "

" . + + "

If you need any assistance, please reach out to hello@untrobotics.com.

" . + + '
' . + + '
' . + "

All the best,

" . + "

UNT Robotics Leadership

" . + '
' . + + "
", + + "hello@untrobotics.com", + null, + [ + [ + 'content' => base64_encode(file_get_contents(BASE . '/images/unt-robotics-email-header.jpg')), + 'type' => 'image/jpeg', + 'filename' => 'unt-robotics-email-header.jpg', + 'disposition' => 'inline', + 'content_id' => 'untrobotics-email-header' + ] + ] + ); + if($email_send_status === false) { + error_log("Failed to email verification token to {$email}."); + echo 'ERROR'; + // delete newly generated token from db since we couldn't email it + $q = $db->query("DELETE FROM + discord_verification_tokens + WHERE + token = '{$token}' + AND + user_id = {$auth['id']}"); + if($q === false){ + error_log("Could not delete discord verification token {$token} for user {$auth['id']}: {$db->error}"); + } + die(); + } + // changes the form to send a token instead of email + echo 'TOKEN'; + break; + } + case "token": + { + $token = $_POST['token']; + if (!ctype_xdigit($token) || strlen($token) !== 6) { + echo 'INVALID_TOKEN'; + die(); + } + global $db; + // check if there's an unexpired token for the user + $q = $db->query("SELECT + unt_email + FROM + discord_verification_tokens + WHERE + token = '{$token}' + AND + user_id = {$auth['id']} + AND + created_on + INTERVAL 7 day > CURRENT_TIMESTAMP()"); + if($q === false){ + error_log("Failed to fetch token for user {$auth['id']}: {$db->error}"); + echo 'ERROR'; + die(); + } + if ($q->num_rows === 0) { + echo 'INVALID_TOKEN'; + die(); + } + // add unt_email to users entry + $email = $q->fetch_column(); + $q = $db->query("UPDATE + users + SET + unt_email = '{$db->real_escape_string($email)}' + WHERE + id = {$auth['id']}"); + if($q === false){ + error_log("Failed to add UNT email ({$email}) to user {$auth['id']}: {$db->error}"); + echo 'ERROR'; + die(); + } + + // deletes the form and shows the discord link + echo 'Click here to update your Discord account status.'; + break; + } +} \ No newline at end of file diff --git a/api/discord/application-commands/commands.json b/api/discord/application-commands/commands.json new file mode 100644 index 00000000..43a15fb9 --- /dev/null +++ b/api/discord/application-commands/commands.json @@ -0,0 +1,30 @@ +[ + { + "name": "verify-email", + "type": 1, + "description": "Verify your identity as a UNT student, faculty, or alumnus", + "options": [ + { + "name": "email", + "description": "A valid UNT email", + "type": 3, + "required": true, + "min_length": 9, + "max_length": 255 + } + ] + }, + { + "name": "verify-token", + "type": 1, + "description": "Verify you own the email provided in /verify-email", + "options": [ + { + "name": "token", + "description": "The token sent to your UNT email address", + "type": 3, + "required": true + } + ] + } +] \ No newline at end of file diff --git a/api/discord/application-commands/register.php b/api/discord/application-commands/register.php new file mode 100644 index 00000000..c20efa3d --- /dev/null +++ b/api/discord/application-commands/register.php @@ -0,0 +1,21 @@ +name . ": " . $e->getMessage(); + } +} + +// log errors +if(count($errs) > 0) { + error_log("Couldn't add/update the following commands to the Discord bot:\n" . implode("\n", $errs)); +} \ No newline at end of file diff --git a/api/discord/bot.php b/api/discord/bot.php index e6c03a0a..468cc5c2 100644 --- a/api/discord/bot.php +++ b/api/discord/bot.php @@ -129,6 +129,24 @@ public static function get_all_users($guild_id) { return static::send_api_request("/guilds/{$guild_id}/members?limit=1000"); } + /** + * Registers an application command for the Discord bot. + * @param $command_json object JSON object with command details + * @param $application_id string The application ID of the Discord bot to register the command to + * @param $guild_id string The guild ID the command will be work on. If empty, command will be registered globally + * @return stdClass + * @throws DiscordBotException + * @see https://discord.com/developers/docs/interactions/application-commands + */ + public static function add_application_command($command_json, $application_id, $guild_id = "") { + $scope = "applications/{$application_id}"; + // if $guild_id is blank, then the scope is global (available in all servers [the bot is in] and DMs + if($guild_id != "") { + $scope .= "/guilds/{$guild_id}"; + } + return self::send_api_request("/v10/{$scope}/commands", 'POST', "application/json", $command_json); + } + // utils public static function hasHitRateLimit($result) { return $result->status_code == 429; diff --git a/api/discord/bots/verifier.php b/api/discord/bots/verifier.php new file mode 100644 index 00000000..d8c7be05 --- /dev/null +++ b/api/discord/bots/verifier.php @@ -0,0 +1,473 @@ +discord = new Discord([ + 'token' => $bot_token, + 'intents' => Intents::getDefaultIntents() + ]); + + // runs when the bot establishes a connection with Discord + $this->discord->on('ready', function (Discord $discord) { + // adds event listener for when an Interaction is started. Interactions are things like slash commands or clicking a button on the bot's message + $discord->on(Event::INTERACTION_CREATE, VerificationBot::AcknowledgeInteraction(...)); + }); + } + + /** + * Starts the webhook. + */ + public function run(){ + $this->discord->run(); + } + + /** + * Handler for {@see Event::INTERACTION_CREATE} events. + * @param Interaction $interaction + * @param Discord $discord + * @return void + */ + private static function AcknowledgeInteraction(Interaction $interaction, Discord $discord) { + // ignore interactions which don't provide data + if (is_null($interaction->data)){ + error_log("Discord verification bot received an unknown interaction initiated by " . $interaction->user->id . " in guild " . $interaction->guild->id . " and channel " . $interaction->channel->id . "."); + return; + } + // handle interactions based on interaction names + switch ($interaction->data->name) { + case "verify-email": + self::ValidateEmail($interaction); + break; + case "verify-token": + self::ValidateToken($interaction); + break; + default: + error_log("Discord verification bot received an unknown interaction with name " . $interaction->data->name); + } + } + + /** + * Validate a /verify-email interaction + * @param Interaction $interaction + * @return void + */ + private static function ValidateEmail(Interaction $interaction) { + // don't do the process if the user has the role already + if ($interaction->member->roles->has(DISCORD_VERIFIED_ROLE_ID)){ + $interaction->respondWithMessage( + MessageBuilder::new()->setContent("You already have the verified role.") + ); + return; + } + + // get email passed to the bot + $email = strtolower($interaction->data->options["email"]->value); + // if email isn't a UNT email, provide error message to user and end interaction + if (!self::IsValidEmail($email)){ + $interaction->respondWithMessage( + MessageBuilder::new()->setContent( + "Invalid email address provided. Make sure the email is a valid UNT email. If you're having issues verifying yourself, contact an officer.". PHP_EOL . PHP_EOL . "Email: ``{$email}``" + ), + true + ); + return; + } + + // user sees an ephemeral, loading message from the bot + $interaction->acknowledgeWithResponse(true)->then( + function () use($interaction, $email) { + global $db; + $escaped_email = $db->real_escape_string($email); + $discord_id = $interaction->user->id; + + // check DB to see if the user already has a UNT email verified + $q = $db->query("SELECT + id + FROM + users + WHERE + unt_email IS NOT NULL + AND + discord_id = {$discord_id} + "); + // db error + if ($q === false) { + error_log("Failed to check verification status for <@{$discord_id}> (email: {$email}) when generating token: {$db->error}"); + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent("Could not generate verification token." . PHP_EOL . PHP_EOL . "If this issue persists, contact an officer.") + ); + return; + } + + // if the user has been verified already, try to add the role instead of generating a token + if ($q->num_rows > 0) { + self::AssignVerfiedRole($interaction, null); + return; + } + + // check for an existing token + $q = $db->query("SELECT + id, created_on + FROM + discord_verification_tokens + WHERE + discord_id = {$discord_id} + ORDER BY + created_on DESC LIMIT 1"); + // db error + if ($q === false) { + error_log("Failed to check for existing tokens for <@{$discord_id}>: {$db->error}"); + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent("Could not generate verification token." . PHP_EOL . PHP_EOL . "If this issue persists, contact an officer.") + ); + return; + } + // token exists + if ($q->num_rows > 0) { + // check for ratelimit + $r = $q->fetch_assoc(); + /** @noinspection PhpUnhandledExceptionInspection */ + + // if ratelimit reached, stop interaction and tell user to wait before generating a new token + /** @noinspection PhpParenthesesCanBeOmittedForNewCallInspection */ + if ((new DateTime())->sub(new DateInterval('PT' . EMAIL_TOKEN_GENERATION_RATE_LIMIT . 'S')) < new DateTime($r['created_on'])) { + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent("Please wait a few minutes before requesting a new token.") + ); + return; + } + + // delete the token from the table so we can generate a new one + $q = $db->query("DELETE FROM discord_verification_tokens WHERE id = {$r['id']}"); + // db error + if ($q === false) { + error_log("Failed to delete verification token for <@" . $discord_id . "> (email: {$email}): {$db->error}"); + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent("Could not generate verification token." . PHP_EOL . PHP_EOL . "If this issue persists, contact an officer.") + ); + return; + } + } + + //generate token + $token = bin2hex(openssl_random_pseudo_bytes(3)); + + // add token to discrd_verification_tokens table + $q = $db->query("INSERT INTO discord_verification_tokens ( + token, + created_on, + discord_id, + unt_email + ) + VALUES ( + '{$token}', + CURRENT_TIMESTAMP(), + $discord_id, + '{$escaped_email}' + )" + ); + + // db error + if (!$q) { + $response = "Could not generate verification token." . PHP_EOL . PHP_EOL . "If this issue persists, contact an officer."; + error_log("Failed to update DB with verification token for <@" . $discord_id . "> (email: {$email}): " . $db->error); + } else { + // Email token + $email_send_status = email( + $email, + "UNT Robotics Discord verification token", + + "
" . + '' . + + '
' . + + '
' . + "

Hey there!

" . + "

Welcome to the team!!

" . + "

Thank you for joining the UNT Robotics Discord server!" . + "

Your verification token is:

" . + "
" . + "
" . + "

{$token}

" . + '
' . + + '
' . + + "

" . + + "

If you need any assistance, please reach out to hello@untrobotics.com.

" . + + '
' . + + '
' . + "

All the best,

" . + "

UNT Robotics Leadership

" . + '
' . + + "
", + + "hello@untrobotics.com", + null, + [ + [ + 'content' => base64_encode(file_get_contents(BASE . '/images/unt-robotics-email-header.jpg')), + 'type' => 'image/jpeg', + 'filename' => 'unt-robotics-email-header.jpg', + 'disposition' => 'inline', + 'content_id' => 'untrobotics-email-header' + ] + ] + ); + + // if the email was successfully sent, respond with a success message. + if ($email_send_status) { + $response = "Email sent to {$email}. Use ``/verify-token `` to continue the verification process. Tokens are valid for 7 days." . PHP_EOL . PHP_EOL . "If you can't find the email, check your spam folder."; + } else { + // if email wasn't sent, respond with an error message and remove the token from the db. + $response = "Could not send an email to {$email}." . PHP_EOL . PHP_EOL . "If this issue persists, contact an officer."; + $q = "DELETE FROM discord_verification_tokens WHERE token = '{$token}' AND discord_id = '{$discord_id}'"; + if ($q === false) { + error_log("Failed to send verification email with a token to {$email}. Also failed to remove the new token entry from the tokens table."); + } + error_log("Failed to send verification email with a token to {$email}."); + } + } + + // update loading message with proper response... any updated response before this is an error message + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent($response) + ); + } + ); + } + + /** + * Validate a /verify-token interaction + * @param Interaction $interaction + * @return void + */ + private static function ValidateToken(Interaction $interaction) { + // if user is already verified (has the verified role), tell them they already have the role and end the interaction + if ($interaction->member->roles->has(DISCORD_VERIFIED_ROLE_ID)){ + $interaction->respondWithMessage( + MessageBuilder::new()->setContent("You already have the verified role.") + ); + return; + } + + global $db; + // get token passed to the bot + // $token sanitized for db since it won't be used elsewhere + $token = strtolower($interaction->data->options["token"]->value); + $discord_id = $interaction->user->id; + // if token isn't valid, tell user verification failed and end interaction + if (!self::IsValidToken($token)){ + $interaction->respondWithMessage( + MessageBuilder::new()->setContent( + "Verification failed." + ), + true + ); + return; + } + // fetch token from db where the user's discord id and token provided match + $q = $db->query("SELECT + id, user_id, unt_email + FROM + discord_verification_tokens + WHERE + token = '{$token}' + AND + discord_id = {$discord_id} + AND + created_on + INTERVAL 7 DAY > CURRENT_TIMESTAMP()"); + // if query fails, respond with an error message, log the db error, and end the interaction + if ($q === false) { + $interaction->respondWithMessage( + MessageBuilder::new()->setContent( + "Verification failed due to internal server error. Contact an officer if this issue persists." + ), + true + ); + error_log("Failed to fetch token ({$token}) from discord verification table for user {$discord_id}: {$db->error}"); + return; + } + // if no entries match token-discord_id combo then fail verification + if ($q->num_rows < 1) { + $interaction->respondWithMessage( + MessageBuilder::new()->setContent( + "Verification failed." + ), + true + ); + return; + } + + $r = $q->fetch_array(MYSQLI_ASSOC); + // user_id isn't null if the token was linked to a user with this discord ID + if ($r['user_id'] != null) { + $interaction->respondWithMessage( + MessageBuilder::new()->setContent( + "Verification failed." + ), + true + ); + return; + } + + self::AssignVerfiedRole($interaction, $token); + self::AddToUsersTable($discord_id, $r['unt_email'], $r['id']); + } + + /** + * Acknowledges the interaction with a loading message, then tries to add the verified role to the user. + * Logs an error if the role update failed. + * @param Interaction $interaction + * @param string|null $token + * @return void + */ + public static function AssignVerfiedRole(Interaction $interaction, mixed $token): void { + $interaction->acknowledgeWithResponse(true)->then(function () use ($interaction, $token) { + $interaction->member->addRole(DISCORD_VERIFIED_ROLE_ID)->then(function () use ($interaction) { + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent( + "You've successfully verified your email address. Welcome to the server." + ), + ); + }, + function ($reason) use ($interaction, $token) { + $interaction->updateOriginalResponse( + MessageBuilder::new()->setContent( + "There was an issue trying to adding the verified role to you. Contact an officer with your token to get your roles updated." + ), + ); + if ($token !== null) { + error_log("Error adding the verified role to user <@{$interaction->user->id}> (ID: {$interaction->user->id}; token: {$token}): $reason"); + } else { + error_log("Error adding the verified role to user <@{$interaction->user->id}> (ID: {$interaction->user->id}; no token required): $reason"); + } + + } + ); + }); + } + + /** + * Adds the user to the users table. + * + * Call after the token has been verified. + * @param int|string $discord_id The user's Discord ID + * @param string $unt_email The user's UNT email. Should not be escaped by real_escape_string()) + * @param string $token_id The ID of the entry in the discord_verification_tokens table that was used to verify the user + * @return void + */ + private static function AddToUsersTable(mixed $discord_id, string $unt_email, string $token_id): void { + global $db; + $unt_email = $db->real_escape_string($unt_email); + + // check if a user registered on the website with the given UNT email + $q = $db->query("SELECT + id + FROM + users + WHERE + email = '{$unt_email}'" + ); + if ($q === false) { + error_log("Failed to add verified user to users table ({$unt_email}, {$discord_id}): {$db->error}"); + return; + } + // if the user exists, update the entry + // else, create a new entry + if ($q->num_rows >= 1) { + $id = $q->fetch_column(); + $query_string = "UPDATE + users + SET + unt_email = '{$unt_email}', + discord_id = {$discord_id} + WHERE + id = {$id}"; + } else { + $query_string = "INSERT INTO + users (unt_email, discord_id) + VALUES ('{$unt_email}', {$discord_id})"; + } + + $q = $db->query($query_string); + if ($q === false) { + if (isset($id)) { + error_log("Error UNT email ({$unt_email}) and Discord ID ({$discord_id}) to user {$id}: {$db->error}"); + } else { + error_log("Error creating user with UNT email {$unt_email} and Discord ID {$discord_id}: {$db->error}"); + } + return; + } + + if (!isset($id)) { + $q = $db->query("SELECT + id + FROM + users + WHERE + unt_email = '{$unt_email}' + AND + discord_id = {$discord_id}"); + if ($q === false) { + error_log("Error fetching newly created user to update token status ($unt_email, {$discord_id}): {$db->error}"); + return; + } + + $id = $q->fetch_column(); + } + + $q = $db->query("UPDATE + discord_verification_tokens + SET + user_id = {$id} + WHERE + id = {$token_id}"); + if ($q === false) { + error_log("Error updating token ($token_id) with user ID: {$db->error}"); + } + } + + /** + * Verify that an email address is a my.unt email + * @param string $email The email to validate + * @return bool True if the email is a UNT email. False otherwise + */ + private static function IsValidEmail(string $email): bool { + // regex for the domain name + $valid_domains = '/@my\.unt\.edu$/i'; + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false && preg_match($valid_domains, $email) === 1; + } + + /** + * Verify that a token is in the accepted format (i.e., hexadecimal) + * @param string $token The token to validate + * @return bool True if the token is in the accepted format. False otherwise + */ + private static function IsValidToken(string $token): bool { + return ctype_xdigit($token); + } +} \ No newline at end of file diff --git a/auth/discord.php b/auth/discord.php index 937fca1b..a896f784 100644 --- a/auth/discord.php +++ b/auth/discord.php @@ -118,6 +118,10 @@ function assign_user_good_standing($user_discord_id) { return AdminBot::add_user_role($user_discord_id); } +function assign_user_verified($user_discord_id) { + return AdminBot::add_user_role($user_discord_id, DISCORD_VERIFIED_ROLE_ID); +} + head('Joined Discord', true, true); // user must be authenticated to reach this point @@ -195,6 +199,15 @@ function assign_user_good_standing($user_discord_id) { // Alert! throw new Exception("Current user is not in good standing."); } + if($untrobotics->is_user_email_verified($userinfo)) { + $assigned = assign_user_verified($user->id); + if ($assigned->status_code != 204) { + error_log("AUTHDIS", var_export($assigned, true)); + throw new Exception("Failed to give user the correct role: " . $assigned->status_code . " ($code)"); + } + } else { + throw new Exception("Current user has not verified their UNT email address."); + } ?>

You're good to go

username; ?>#discriminator; ?> has been given the Good Standing role.
@@ -228,7 +241,7 @@ function assign_user_good_standing($user_discord_id) { } } - AdminBot::send_message("[AUTHDIS] Failed to assign '{$userinfo['name']}' [{$username}#{$discriminator}] (http://untro.bo/admin/check-good-standing?u={$userinfo['id']}) to the Good Standing role.\n{$ex}"); + AdminBot::send_message("[AUTHDIS] Failed to assign '{$userinfo['name']}' [{$username}#{$discriminator}] (http://untro.bo/admin/check-good-standing?u={$userinfo['id']}) to the Good Standing or verified role.\n{$ex}"); ?>

Error!

diff --git a/auth/join.php b/auth/join.php index 0c6c61c8..8bfe8779 100644 --- a/auth/join.php +++ b/auth/join.php @@ -73,9 +73,28 @@ $timezone = "UTC"; } } - - // do query - $q = $db->query('INSERT INTO users (name, email, phone, unteuid, grad_term, grad_year, password, reg_timestamp, reg_ip, timezone) + // update user that was added from the discord verification process + $q = $db->query('SELECT id FROM users WHERE unt_email = "' . $db->real_escape_string($email) . '"'); + if($q && $q->num_rows > 0){ + $id = $q->fetch_array(MYSQLI_ASSOC)['id']; + $query_string = "UPDATE + users + SET + name = '{$db->real_escape_string($name)}', + email = '{$db->real_escape_string($email)}', + phone = '{$db->real_escape_string($phone)}', + unteuid = '{$db->real_escape_string($unteuid)}', + grad_term = '" . $db->real_escape_string(array_search($grad_term, $valid_grad_terms)) ."', + grad_year = '{$db->real_escape_string($grad_year)}', + password = '" . $db->real_escape_string(password_hash($password1, PASSWORD_BCRYPT, array('cost' => 12))) . "', + reg_timestamp = NOW(), + reg_ip = '{$db->real_escape_string($ip)}', + timezone = '{$db->real_escape_string($timezone)}' + WHERE + id = {$id} + "; + } else { + $query_string = 'INSERT INTO users (name, email, phone, unteuid, grad_term, grad_year, password, reg_timestamp, reg_ip, timezone) VALUES ( "' . $db->real_escape_string($name) . '", "' . $db->real_escape_string($email) . '", @@ -88,7 +107,10 @@ "' . $db->real_escape_string($ip) . '", "' . $db->real_escape_string($timezone) . '" ) - '); + '; + } + // do query + $q = $db->query($query_string); if ($q) { // set cookies @@ -96,7 +118,8 @@ $auth_session_id = obfuscate_hash(sha1($fingerprint . session_id())); // based on IP, time, /dev/urandom and a PHP PRNG (PLCG) and fingerprint calculated above session_regenerate_id(); $auth_session_name = obfuscate_hash(bin2hex(random_bytes(32))); // just really random - + if(!isset($id)) + $id = $db->insert_id; $db->query("INSERT INTO auth_sessions (session_id, session_name, @@ -108,7 +131,7 @@ ('".$db->real_escape_string($auth_session_id)."', '".$db->real_escape_string($auth_session_name)."', '".$db->real_escape_string($fingerprint)."', - '".$db->real_escape_string($db->insert_id)."', + '".$db->real_escape_string($id)."', '".$db->real_escape_string(0)."') ") or die($db->error); // remove this for security @@ -118,6 +141,7 @@ header('Location: /auth/welcome'); } else { $error = 'An internal error occurred, please contact support.'; + require_once(__DIR__ . '/../api/discord/bots/admin.php'); AdminBot::send_message("Failed to register new user due to database error: $db->error. Please investigate."); } } diff --git a/dues/paid.php b/dues/paid.php index 6fe114c7..40cb5121 100644 --- a/dues/paid.php +++ b/dues/paid.php @@ -1,6 +1,7 @@ Dues Paid

Thank you for paying your dues.

- + Click here to update your Discord account status. + +
+
+
Next, please verify your UNT email address.
+
+
+
+ + + + + +
+ +
+
+
diff --git a/js/script.js b/js/script.js index 20181061..1a1febe8 100644 --- a/js/script.js +++ b/js/script.js @@ -1583,6 +1583,50 @@ $document.ready(function () { $.getScript("//www.google.com/recaptcha/api.js?onload=onloadCaptchaCallback&render=explicit&hl=en"); } + /** + * Finalizes a form's state after submission upon a successful (200) response from the server + * @param form The jQuery selector for the form + * @param formHasCaptcha A boolean representing whether the form has a captcha or not + * @param output The jQuery selector for where output text will be placed + * @param result The server response message + * @param msg An object or associative array which maps result to a message to put in output + */ + function finalizeForm(form, formHasCaptcha, output, result, msg) { + form + .addClass('success') + .removeClass('form-in-process'); + + if (formHasCaptcha) { + grecaptcha.reset(); + } + + //result = result.length == 5 ? result : 'MF255'; + output.text(msg[result]); + + if (result === "SUCCESS") { + if (output.hasClass("snackbars")) { + output.css('background-color', '#24c57c'); + output.html('

' + msg[result] + '

'); + } else { + output.addClass("active success"); + } + form.clearForm(); + } else { + if (output.hasClass("snackbars")) { + output.html('

' + msg[result] + '

'); + } else { + output.addClass("active error"); + } + } + + form.find('input, textarea').blur(); + + setTimeout(function () { + output.removeClass("active error success"); + form.removeClass('success'); + }, 3500); + } + /** * RD Mailform */ @@ -1677,9 +1721,9 @@ $document.ready(function () { error: function (result) { var form = $(plugins.rdMailForm[this.extraData.counter]), output = $("#" + $(plugins.rdMailForm[this.extraData.counter]).attr("data-form-output")); - output.text(msg[result]); + output.text(msg[result.responseText]); + output.addClass('active error') form.removeClass('form-in-process'); - if (formHasCaptcha) { grecaptcha.reset(); } @@ -1688,40 +1732,8 @@ $document.ready(function () { var form = $(plugins.rdMailForm[this.extraData.counter]), output = $("#" + form.attr("data-form-output")); - form - .addClass('success') - .removeClass('form-in-process'); - - if (formHasCaptcha) { - grecaptcha.reset(); - } - - //result = result.length == 5 ? result : 'MF255'; - output.text(msg[result]); - - if (result === "SUCCESS") { - if (output.hasClass("snackbars")) { - output.css('background-color', '#24c57c'); - output.html('

' + msg[result] + '

'); - } else { - output.addClass("active success"); - } - form.clearForm(); - } else { - if (output.hasClass("snackbars")) { - output.html('

' + msg[result] + '

'); - } else { - output.addClass("active error"); - } - } - - form.find('input, textarea').blur(); - - setTimeout(function () { - output.removeClass("active error success"); - form.removeClass('success'); - }, 3500); - } + finalizeForm(form, formHasCaptcha, output, result, msg); + } }); } } @@ -1922,6 +1934,68 @@ $document.ready(function () { }, 300); }); } + + /** + * Custom email verification AJAX form + */ + // noinspection JSJQueryEfficiency + if ($('form#verify-email').length) { + let msg = { + "INVALID_EMAIL": "The e-mail address you entered is not valid. Please ensure the email ends with @my.unt.edu.", + "INVALID_TOKEN": "The token is incorrect or invalid.", + "INVALID_LOGIN": "Your log-in session has expired or isn't valid anymore. Please log-in again to continue.", + "INVALID_REQUEST": "The request fields are invalid.", + "TOKEN_RATELIMIT": "Please wait a few minutes before requesting a new token.", + "TOKEN": "Token sent.", + "SUCCESS": "E-mail successfully verified.", + "ERROR": "A server error occurred. Please contact support." + } + let $form = $('form#verify-email'); + let $output = $('#' + $form.attr('data-form-output')) + $form.ajaxForm({ + beforeSubmit: function () { + if(isValidated($form.find('input:not([type="hidden"])'), false)) { + $output.removeClass("active error success"); + $form.addClass('form-in-process'); + if ($output.hasClass("snackbars")) { + $output.html('

Sending

'); + $output.addClass("active"); + } + return true + } + return false + }, + error: function (result) { + $output.text(msg[result.responseText]).addClass('active error') + }, + success: function (result) { + if (result === "TOKEN") { + let $input = $form.find('input#email') + $form.find('label#email-label').addClass('hidden') + let email = $input.val(); + $input.attr({ + 'type': 'hidden', + 'disabled': '' + }) + $input = $form.find('input#token') + $input.attr('type', 'text').removeAttr('disabled') + $form.find('label#token-label').removeClass('hidden') + $form.find('input#type').val('token') + $form.siblings('div').children('h6').text("We’ve sent a verification token to " + email + ". Please check your inbox and spam/junk folder and enter the code below to verify your address.") + $form.find('button').text('Verify') + finalizeForm($form,false, $output, result, msg) + } else if (result.startsWith("INVALID") || result.startsWith('TOKEN_RATELIMIT') || result.startsWith("ERROR")) { + finalizeForm($form, false, $output, result, msg) + } else { + let $section = $('section#email-verification-section'); + $section.parent().append(result) + $section.remove() + finalizeForm($form, false, $output, "SUCCESS", msg) + } + }, + headers: {'Cookie' : document.cookie }, + }) + } }); diff --git a/sql/discord_verification_tokens.sql b/sql/discord_verification_tokens.sql new file mode 100644 index 00000000..acf0383f --- /dev/null +++ b/sql/discord_verification_tokens.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `discord_verification_tokens`; +CREATE TABLE `discord_verification_tokens`( + `id` int(11) NOT NULL AUTO_INCREMENT, + `token` varchar(6) NOT NULL, + `created_on` timestamp NOT NULL, + `discord_id` bigint(20), + `user_id` int(11), + `unt_email` varchar(255) NOT NULL, + PRIMARY KEY(id), + FOREIGN KEY (user_id) REFERENCES users(id) +) AUTO_INCREMENT=8 DEFAULT CHARSET=latin1; \ No newline at end of file diff --git a/sql/migrations/URW-167-unt-email-field.sql b/sql/migrations/URW-167-unt-email-field.sql new file mode 100644 index 00000000..f79a9c0b --- /dev/null +++ b/sql/migrations/URW-167-unt-email-field.sql @@ -0,0 +1,2 @@ +ALTER TABLE users + ADD COLUMN unt_email varchar(255); diff --git a/template/classes/untrobotics.php b/template/classes/untrobotics.php index f554f25f..00c7bf52 100644 --- a/template/classes/untrobotics.php +++ b/template/classes/untrobotics.php @@ -70,6 +70,28 @@ public function is_user_in_good_standing($userinfo) { return $q->num_rows === 1; } + public function is_user_email_verified($userinfo) { + $uid = null; + if (is_array($userinfo)) { + $uid = $userinfo['id']; + } else { + $uid = $userinfo; + } + $q = $this->db->query(' + SELECT id FROM users + WHERE + id = "' . $this->db->real_escape_string($uid) . '" + AND + unt_email IS NOT NULL + '); + + if (!$q) { + return false; + } + + return $q->num_rows === 1; + } + // semester, dues functions public function get_current_term() { return $this->get_term_from_date(time()); diff --git a/template/sample.config.php b/template/sample.config.php index 67970de4..0f27f7f8 100644 --- a/template/sample.config.php +++ b/template/sample.config.php @@ -86,6 +86,8 @@ '755952946566660206' // Web/Ops Team ) ); +define('DISCORD_VERIFIED_ROLE_ID', '1414807269580734527'); +define('EMAIL_TOKEN_GENERATION_RATE_LIMIT', 300); define('PAYPAL_PDT_ID_TOKEN', ''); define('PAYPAL_SANDBOX_PDT_ID_TOKEN', '');