diff --git a/.gitignore b/.gitignore index 1f25c88..58d21fe 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ secret.key *.ipr *.iws .vscode/launch.json + +/.vs diff --git a/api/common.go b/api/common.go index 6abad39..0f93ca4 100644 --- a/api/common.go +++ b/api/common.go @@ -68,7 +68,11 @@ func Init(mux *http.ServeMux) error { mux.HandleFunc("/auth/{provider}/logout", handleProviderLogout) // admin - mux.HandleFunc("POST /admin/account/discord-link", handleAdminDiscordLink) + mux.HandleFunc("POST /admin/account/discordLink", handleAdminDiscordLink) + mux.HandleFunc("POST /admin/account/discordUnlink", handleAdminDiscordUnlink) + mux.HandleFunc("POST /admin/account/googleLink", handleAdminGoogleLink) + mux.HandleFunc("POST /admin/account/googleUnlink", handleAdminGoogleUnlink) + mux.HandleFunc("GET /admin/account/adminSearch", handleAdminSearch) return nil } diff --git a/api/endpoints.go b/api/endpoints.go index d9fb802..162900d 100644 --- a/api/endpoints.go +++ b/api/endpoints.go @@ -693,13 +693,257 @@ func handleAdminDiscordLink(w http.ResponseWriter, r *http.Request) { return } - err = db.AddDiscordIdByUsername(r.Form.Get("discordId"), r.Form.Get("username")) + username := r.Form.Get("username") + discordId := r.Form.Get("discordId") + + // this does a quick call to make sure the username exists on the server before allowing the rest of the code to run + // this calls error value 404 (StatusNotFound) if there's no data; this means the username does not exist in the server + _, err = db.CheckUsernameExists(username) + if err != nil { + httpError(w, r, fmt.Errorf("username does not exist on the server"), http.StatusNotFound) + return + } + + userUuid, err := db.FetchUUIDFromUsername(username) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + + err = db.AddDiscordIdByUUID(discordId, userUuid) if err != nil { httpError(w, r, err, http.StatusInternalServerError) return } - log.Printf("%s: %s added discord id %s to username %s", r.URL.Path, userDiscordId, r.Form.Get("discordId"), r.Form.Get("username")) + log.Printf("%s: %s added discord id %s to username %s", r.URL.Path, userDiscordId, discordId, username) + + w.WriteHeader(http.StatusOK) +} + +func handleAdminDiscordUnlink(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + httpError(w, r, fmt.Errorf("failed to parse request form: %s", err), http.StatusBadRequest) + return + } + + uuid, err := uuidFromRequest(r) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + userDiscordId, err := db.FetchDiscordIdByUUID(uuid) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + hasRole, err := account.IsUserDiscordAdmin(userDiscordId, account.DiscordGuildID) + if !hasRole || err != nil { + httpError(w, r, fmt.Errorf("user does not have the required role"), http.StatusForbidden) + return + } + + username := r.Form.Get("username") + discordId := r.Form.Get("discordId") + + switch { + case username != "": + log.Printf("Username given, removing discordId") + // this does a quick call to make sure the username exists on the server before allowing the rest of the code to run + // this calls error value 404 (StatusNotFound) if there's no data; this means the username does not exist in the server + _, err = db.CheckUsernameExists(username) + if err != nil { + httpError(w, r, fmt.Errorf("username does not exist on the server"), http.StatusNotFound) + return + } + + userUuid, err := db.FetchUUIDFromUsername(username) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + + err = db.RemoveDiscordIdByUUID(userUuid) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + case discordId != "": + log.Printf("DiscordID given, removing discordId") + err = db.RemoveDiscordIdByDiscordId(discordId) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + } + + log.Printf("%s: %s removed discord id %s from username %s", userDiscordId, r.URL.Path, r.Form.Get("discordId"), r.Form.Get("username")) w.WriteHeader(http.StatusOK) } + +func handleAdminGoogleLink(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + httpError(w, r, fmt.Errorf("failed to parse request form: %s", err), http.StatusBadRequest) + return + } + + uuid, err := uuidFromRequest(r) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + userDiscordId, err := db.FetchDiscordIdByUUID(uuid) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + hasRole, err := account.IsUserDiscordAdmin(userDiscordId, account.DiscordGuildID) + if !hasRole || err != nil { + httpError(w, r, fmt.Errorf("user does not have the required role"), http.StatusForbidden) + return + } + + username := r.Form.Get("username") + googleId := r.Form.Get("googleId") + + // this does a quick call to make sure the username exists on the server before allowing the rest of the code to run + // this calls error value 404 (StatusNotFound) if there's no data; this means the username does not exist in the server + _, err = db.CheckUsernameExists(username) + if err != nil { + httpError(w, r, fmt.Errorf("username does not exist on the server"), http.StatusNotFound) + return + } + + userUuid, err := db.FetchUUIDFromUsername(username) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + + err = db.AddGoogleIdByUUID(googleId, userUuid) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + + log.Printf("%s: %s added google id %s to username %s", r.URL.Path, userDiscordId, googleId, username) + + w.WriteHeader(http.StatusOK) +} + +func handleAdminGoogleUnlink(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + httpError(w, r, fmt.Errorf("failed to parse request form: %s", err), http.StatusBadRequest) + return + } + + uuid, err := uuidFromRequest(r) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + userDiscordId, err := db.FetchDiscordIdByUUID(uuid) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + hasRole, err := account.IsUserDiscordAdmin(userDiscordId, account.DiscordGuildID) + if !hasRole || err != nil { + httpError(w, r, fmt.Errorf("user does not have the required role"), http.StatusForbidden) + return + } + + username := r.Form.Get("username") + googleId := r.Form.Get("googleId") + + switch { + case username != "": + log.Printf("Username given, removing googleId") + // this does a quick call to make sure the username exists on the server before allowing the rest of the code to run + // this calls error value 404 (StatusNotFound) if there's no data; this means the username does not exist in the server + _, err = db.CheckUsernameExists(username) + if err != nil { + httpError(w, r, fmt.Errorf("username does not exist on the server"), http.StatusNotFound) + return + } + + userUuid, err := db.FetchUUIDFromUsername(username) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + + err = db.RemoveGoogleIdByUUID(userUuid) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + case googleId != "": + log.Printf("DiscordID given, removing googleId") + err = db.RemoveGoogleIdByDiscordId(googleId) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + } + + log.Printf("%s: %s removed google id %s from username %s", userDiscordId, r.URL.Path, r.Form.Get("googleId"), r.Form.Get("username")) + + w.WriteHeader(http.StatusOK) +} + +func handleAdminSearch(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + httpError(w, r, fmt.Errorf("failed to parse request form: %s", err), http.StatusBadRequest) + return + } + + uuid, err := uuidFromRequest(r) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + userDiscordId, err := db.FetchDiscordIdByUUID(uuid) + if err != nil { + httpError(w, r, err, http.StatusUnauthorized) + return + } + + hasRole, err := account.IsUserDiscordAdmin(userDiscordId, account.DiscordGuildID) + if !hasRole || err != nil { + httpError(w, r, fmt.Errorf("user does not have the required role"), http.StatusForbidden) + return + } + + username := r.Form.Get("username") + + // this does a quick call to make sure the username exists on the server before allowing the rest of the code to run + // this calls error value 404 (StatusNotFound) if there's no data; this means the username does not exist in the server + _, err = db.CheckUsernameExists(username) + if err != nil { + httpError(w, r, fmt.Errorf("username does not exist on the server"), http.StatusNotFound) + return + } + + // this does a single call that does a query for multiple columns from our database and makes an object out of it, which is returned to us + adminSearchResult, err := db.FetchAdminDetailsByUsername(username) + if err != nil { + httpError(w, r, err, http.StatusInternalServerError) + return + } + + writeJSON(w, r, adminSearchResult) + log.Printf("%s: %s searched for username %s", userDiscordId, r.URL.Path, username) +} diff --git a/beta.env b/beta.env new file mode 100644 index 0000000..d472e37 --- /dev/null +++ b/beta.env @@ -0,0 +1,13 @@ +VITE_BYPASS_LOGIN=0 +VITE_BYPASS_TUTORIAL=0 +VITE_SERVER_URL=http://{serverIP}:8001 +VITE_DISCORD_CLIENT_ID=1248062921129459756 +VITE_GOOGLE_CLIENT_ID=955345393540-2k6lfftf0fdnb0krqmpthjnqavfvvf73.apps.googleusercontent.com +VITE_I18N_DEBUG=1 +debug=true +dbaddr=db +dbuser=pokerogue +dbpass=pokerogue +dbname=pokeroguedb +gameurl=http://{devMachineIP}:8000 +callbackurl=http://{serverIP}:8001 diff --git a/db/account.go b/db/account.go index d628df7..3ee9633 100644 --- a/db/account.go +++ b/db/account.go @@ -68,6 +68,25 @@ func AddGoogleIdByUsername(googleId string, username string) error { return nil } +func AddGoogleIdByUUID(googleId string, uuid []byte) error { + _, err := handle.Exec("UPDATE accounts SET googleId = ? WHERE uuid = ?", googleId, uuid) + if err != nil { + return err + } + + return nil +} + +func AddDiscordIdByUUID(discordId string, uuid []byte) error { + _, err := handle.Exec("UPDATE accounts SET discordId = ? WHERE uuid = ?", discordId, uuid) + if err != nil { + return err + } + + return nil +} + + func FetchUsernameByDiscordId(discordId string) (string, error) { var username string err := handle.QueryRow("SELECT username FROM accounts WHERE discordId = ?", discordId).Scan(&username) @@ -154,6 +173,60 @@ func FetchUsernameBySessionToken(token []byte) (string, error) { return username, nil } +func CheckUsernameExists(username string) (string, error) { + var dbUsername sql.NullString + err := handle.QueryRow("SELECT username FROM accounts WHERE username = ?", username).Scan(&dbUsername) + if err != nil { + return "", err + } + if !dbUsername.Valid { + return "", nil + } + + return dbUsername.String, nil +} + +func FetchLastLoggedInDateByUsername(username string) (string, error) { + var lastLoggedIn sql.NullString + err := handle.QueryRow("SELECT lastLoggedIn FROM accounts WHERE username = ?", username).Scan(&lastLoggedIn) + if err != nil { + return "", err + } + if !lastLoggedIn.Valid { + return "", nil + } + + return lastLoggedIn.String, nil +} + +type AdminSearchResponse struct { + Username string `json:"username"` + DiscordId string `json:"discordId"` + GoogleId string `json:"googleId"` + LastLoggedIn string `json:"lastLoggedIn"` + Registered string `json:"registered"` +} + +func FetchAdminDetailsByUsername(dbUsername string) (AdminSearchResponse, error) { + var resultUsername, resultDiscordId, resultGoogleId, resultLastLoggedIn, resultRegistered sql.NullString + var adminResponse AdminSearchResponse + + err := handle.QueryRow("SELECT username, discordId, googleId, lastLoggedIn, registered from accounts WHERE username = ?", dbUsername).Scan(&resultUsername, &resultDiscordId, &resultGoogleId, &resultLastLoggedIn, &resultRegistered) + if err != nil { + return adminResponse, err + } + + adminResponse = AdminSearchResponse{ + Username: resultUsername.String, + DiscordId: resultDiscordId.String, + GoogleId: resultGoogleId.String, + LastLoggedIn: resultLastLoggedIn.String, + Registered: resultRegistered.String, + } + + return adminResponse, nil +} + func UpdateAccountPassword(uuid, key, salt []byte) error { _, err := handle.Exec("UPDATE accounts SET (hash, salt) VALUES (?, ?) WHERE uuid = ?", key, salt, uuid) if err != nil { @@ -343,6 +416,16 @@ func FetchUsernameFromUUID(uuid []byte) (string, error) { return username, nil } +func FetchUUIDFromUsername(username string) ([]byte, error) { + var uuid []byte + err := handle.QueryRow("SELECT uuid FROM accounts WHERE username = ?", username).Scan(&uuid) + if err != nil { + return nil, err + } + + return uuid, nil +} + func RemoveDiscordIdByUUID(uuid []byte) error { _, err := handle.Exec("UPDATE accounts SET discordId = NULL WHERE uuid = ?", uuid) if err != nil { @@ -360,3 +443,39 @@ func RemoveGoogleIdByUUID(uuid []byte) error { return nil } + +func RemoveGoogleIdByUsername(username string) error { + _, err := handle.Exec("UPDATE accounts SET googleId = NULL WHERE username = ?", username) + if err != nil { + return err + } + + return nil +} + +func RemoveDiscordIdByUsername(username string) error { + _, err := handle.Exec("UPDATE accounts SET discordId = NULL WHERE username = ?", username) + if err != nil { + return err + } + + return nil +} + +func RemoveDiscordIdByDiscordId(discordId string) error { + _, err := handle.Exec("UPDATE accounts SET discordId = NULL WHERE discordId = ?", discordId) + if err != nil { + return err + } + + return nil +} + +func RemoveGoogleIdByDiscordId(discordId string) error { + _, err := handle.Exec("UPDATE accounts SET googleId = NULL WHERE discordId = ?", discordId) + if err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/docker-compose.Example.yml b/docker-compose.Example.yml index 412a69d..b8d916a 100644 --- a/docker-compose.Example.yml +++ b/docker-compose.Example.yml @@ -37,6 +37,8 @@ services: - database:/var/lib/mysql networks: - internal + ports: + - "3306:3306" # Watchtower is a service that will automatically update your running containers # when a new image is available. This is useful for keeping your server up-to-date.