1+ #ifndef CHANGE_USERNAME_COMMAND_HEADER
2+ #define CHANGE_USERNAME_COMMAND_HEADER
3+
4+ #include " ../ICommand.h"
5+ #include " ../ChatCommands.h"
6+ #include " Utils/Utils.h"
7+ #include " ../../MainServer.h"
8+ #include < source_location>
9+ #include < regex>
10+ #include < libbcrypt/include/bcrypt/BCrypt.hpp>
11+ #include < libcppotp/auth.h>
12+
13+ namespace Main
14+ {
15+ namespace Command
16+ {
17+ class ChangeUsername final : public ICommand
18+ {
19+ private:
20+ std::string m_password{};
21+ std::string m_2faToken{};
22+ std::string m_newUsername{};
23+
24+ bool validatePassword (const std::string& plain, std::string hash)
25+ {
26+ if (BCrypt::validatePassword (plain, hash))
27+ return true ;
28+
29+ if (hash.substr (0 , 4 ) == " $2b$" )
30+ {
31+ std::string hash_2a = hash;
32+ hash_2a[2 ] = ' a' ;
33+ return BCrypt::validatePassword (plain, hash_2a);
34+ }
35+
36+ return false ;
37+ }
38+
39+ bool parseCommand (const std::string& providedCommand) override
40+ {
41+ std::smatch match;
42+ if (std::regex_match (providedCommand, match, this ->m_pattern ))
43+ {
44+ m_password = match[2 ].str ();
45+ m_2faToken = match[3 ].str ();
46+ m_newUsername = match[4 ].str ();
47+ return true ;
48+ }
49+ return false ;
50+ }
51+
52+ bool verifyToken (const std::string& encryptedSecret, const std::optional<std::string>& token)
53+ {
54+ if (encryptedSecret.empty () || !token.has_value ()) return false ;
55+
56+ auto optSecret = Common::Utils::decrypt2FASecret (encryptedSecret,
57+ Common::Utils::SetupParser::getInstance ().getGeneralSetup ().twoFaSecret );
58+ if (!optSecret) return false ;
59+
60+ const int t_interval = 30 ;
61+ std::time_t now = std::time (nullptr );
62+
63+ for (int i = -1 ; i <= 1 ; ++i)
64+ {
65+ auto expectedToken = auth::generateToken (optSecret.value (), now + i * t_interval, t_interval);
66+ std::ostringstream oss;
67+ oss << std::setw (6 ) << std::setfill (' 0' ) << expectedToken;
68+
69+ if (oss.str () == token.value ()) return true ;
70+ }
71+
72+ return false ;
73+ }
74+
75+ bool validateUsername (const std::string& username)
76+ {
77+ if (username.length () < 4 || username.length () > 20 )
78+ return false ;
79+
80+ if (!std::isalpha (username[0 ]))
81+ return false ;
82+
83+ for (char c : username)
84+ {
85+ if (!std::isalnum (c) && c != ' _' )
86+ return false ;
87+ }
88+
89+ if (username.find (" __" ) != std::string::npos)
90+ return false ;
91+
92+ bool hasLetter = false ;
93+ for (char c : username)
94+ {
95+ if (std::isalpha (c))
96+ {
97+ hasLetter = true ;
98+ break ;
99+ }
100+ }
101+
102+ return hasLetter;
103+ }
104+
105+ public:
106+ explicit ChangeUsername (const Common::Enums::PlayerGrade requiredGrade)
107+ : ICommand{ requiredGrade, " /changeusername <Password> <2faToken> <NewUsername>" ,
108+ R"( ^(\S+)\s*(\S+)\s*(\S+)\s*(\S+)\s*$)" }
109+ {
110+ }
111+
112+ void execute (const std::string& command, std::shared_ptr<Main::Network::Session> session,
113+ MN::SessionsManager& sessionsManager, MC::RoomsManager&, MP::MainScheduler& scheduler,
114+ std::uint32_t , Main::MainServer& server) override
115+ {
116+ const auto & ainfo = session->getAccountInfo ();
117+ if (ainfo.playerGrade >= Common::Enums::PlayerGrade::GRADE_MOD)
118+ {
119+ session->sendMessage (" error: staff accounts cannot change username. Contact admins." );
120+ return ;
121+ }
122+
123+ if (!parseCommand (command))
124+ {
125+ session->sendMessage (" error: invalid command format. Usage: /changeusername <Password> <2faToken> <NewUsername>" );
126+ return ;
127+ }
128+
129+ if (!validateUsername (m_newUsername))
130+ {
131+ session->sendMessage (" error: invalid username. Must be 4-20 characters, start with a letter, and contain only letters, numbers, and underscores (no consecutive underscores)" );
132+ return ;
133+ }
134+
135+ auto encryptedEmail = scheduler.immediatePersist (std::source_location::current (),
136+ &Main::Persistence::PersistentDatabase::getColumnByAid, " Email" , ainfo.accountID );
137+
138+ if (!encryptedEmail || encryptedEmail->empty ())
139+ {
140+ session->sendMessage (" error: account does not have a registered email. Username change not allowed." );
141+ return ;
142+ }
143+
144+ auto decryptedEmail = Common::Utils::decryptEmail (encryptedEmail.value (),
145+ Common::Utils::SetupParser::getInstance ().getGeneralSetup ().emailSecret );
146+
147+ if (!decryptedEmail)
148+ {
149+ session->sendMessage (" error: failed to fetch email from database." );
150+ return ;
151+ }
152+
153+ bool usernameExists = scheduler.immediatePersist (std::source_location::current (),
154+ &Main::Persistence::PersistentDatabase::checkUsernameExists, m_newUsername);
155+
156+ if (usernameExists)
157+ {
158+ session->sendMessage (" error: username already taken" );
159+ return ;
160+ }
161+
162+ auto hashedPassword = scheduler.immediatePersist (std::source_location::current (),
163+ &Main::Persistence::PersistentDatabase::getColumnByAid, " Password" , ainfo.accountID );
164+
165+ if (!hashedPassword.has_value ())
166+ {
167+ session->sendMessage (" error: failed to retrieve account data" );
168+ return ;
169+ }
170+
171+ const std::uint32_t maxAttempts = 5 ;
172+
173+ if (!validatePassword (m_password, hashedPassword.value ()))
174+ {
175+ session->m_totalWrongUsernameChange ++;
176+
177+ if (session->m_totalWrongUsernameChange >= maxAttempts)
178+ {
179+ sessionsManager.removeSession (ainfo.uniqueId .session );
180+ if (!session->banAccount (9999 , " [Automatic] Too many wrong passwords while changing username" ,
181+ Common::Enums::GRADE_SYSTEM))
182+ {
183+ session->sendMessage (" error: unknown error" );
184+ }
185+ else
186+ {
187+ auto subject = " Your TB Account Was Banned" ;
188+ auto body = " Hello " + std::string (ainfo.nickname ) +
189+ " , your TB account was automatically banned as you attempted to change your username "
190+ " but provided the wrong password " + std::to_string (maxAttempts) +
191+ " consecutive times! Contact support for help." ;
192+
193+ server.emailDispatcher .sendEmailAsync (
194+ { decryptedEmail.value () }, subject, body,
195+ [accountId = ainfo.accountID , &scheduler, this ]() {
196+ scheduler.immediatePersist (std::source_location::current (),
197+ &Main::Persistence::PersistentDatabase::logGameEvent,
198+ " EmailUsernameChange" ,
199+ " Failed to send account ban email for accountID: " + std::to_string (accountId),
200+ " HIGH" );
201+ });
202+ }
203+ return ;
204+ }
205+
206+ session->sendMessage (" error: password is incorrect" );
207+ return ;
208+ }
209+
210+ auto encryptedSecret = scheduler.immediatePersist (std::source_location::current (),
211+ &Main::Persistence::PersistentDatabase::getColumnByAid, " Secret" , ainfo.accountID );
212+
213+ if (!encryptedSecret.has_value ())
214+ {
215+ session->sendMessage (" error: failed to retrieve 2FA data" );
216+ return ;
217+ }
218+
219+ if (!verifyToken (encryptedSecret.value (), m_2faToken))
220+ {
221+ session->m_totalWrong2FaUsernameChange ++;
222+
223+ if (session->m_totalWrong2FaUsernameChange >= maxAttempts)
224+ {
225+ sessionsManager.removeSession (ainfo.uniqueId .session );
226+ if (!session->banAccount (9999 , " [Automatic] Too many wrong 2FAs while changing username" ,
227+ Common::Enums::GRADE_SYSTEM))
228+ {
229+ session->sendMessage (" error: unknown error" );
230+ }
231+ else
232+ {
233+ auto subject = " Your TB Account Was Banned" ;
234+ auto body = " Hello " + std::string (ainfo.nickname ) +
235+ " , your TB account was automatically banned as you attempted to change your username "
236+ " but provided the wrong 2FA token " + std::to_string (maxAttempts) +
237+ " consecutive times! Contact support for help." ;
238+
239+ server.emailDispatcher .sendEmailAsync (
240+ { decryptedEmail.value () }, subject, body,
241+ [accountId = ainfo.accountID , &scheduler, this ]() {
242+ scheduler.immediatePersist (std::source_location::current (),
243+ &Main::Persistence::PersistentDatabase::logGameEvent,
244+ " EmailUsernameChange" ,
245+ " Failed to send account ban email for accountID: " + std::to_string (accountId),
246+ " HIGH" );
247+ });
248+ }
249+ return ;
250+ }
251+
252+ session->sendMessage (" error: invalid 2FA token" );
253+ return ;
254+ }
255+
256+ bool success = scheduler.immediatePersist (std::source_location::current (),
257+ &Main::Persistence::PersistentDatabase::updateUsernameByAid,
258+ ainfo.accountID , m_newUsername, ainfo.nickname );
259+
260+ if (!success)
261+ {
262+ session->sendMessage (" error: failed to update username" );
263+ return ;
264+ }
265+
266+ session->m_totalWrongUsernameChange = 0 ;
267+ session->m_totalWrong2FaUsernameChange = 0 ;
268+
269+
270+ auto subject = " Your ToyBattles Username Has Been Changed" ;
271+ auto body = " Hello " + m_newUsername + " ,\n\n "
272+ " Your username has been successfully changed " +
273+ " ' to '" + m_newUsername + " '.\n\n "
274+ " If you did not make this change, please contact support immediately.\n\n "
275+ " Regards,\n ToyBattles Team" ;
276+
277+ server.emailDispatcher .sendEmailAsync (
278+ { decryptedEmail.value () }, subject, body,
279+ [accountId = ainfo.accountID , &scheduler, this ]() {
280+ scheduler.immediatePersist (std::source_location::current (),
281+ &Main::Persistence::PersistentDatabase::logGameEvent,
282+ " EmailUsernameChange" ,
283+ " Failed to send username change confirmation email for accountID: " +
284+ std::to_string (accountId), " MEDIUM" );
285+ });
286+
287+ session->sendMessage (" success: your username has been changed to '" + m_newUsername + " '. You will need to relog with your new username." );
288+ }
289+ };
290+
291+ REGISTER_CMD (ChangeUsername, Common::Enums::PlayerGrade::GRADE_NORMAL)
292+ };
293+ }
294+
295+ #endif
0 commit comments