From 9efdaf7aa341a7d6e8aacfc0c4c91d00a96a7faf Mon Sep 17 00:00:00 2001 From: Leijurv Date: Tue, 11 Feb 2020 00:39:35 -0800 Subject: [PATCH] automated donation processing --- go.mod | 2 +- go.sum | 2 + src/api/v1/donate.go | 35 ++++--- src/api/v1/register.go | 94 +++++++++++++++++++ src/api/v1/server.go | 5 +- src/database/schema.go | 15 +++ src/discord/discord.go | 48 ++++++++++ src/jwt/jwt.go | 23 +---- src/jwt/minecraft.go | 3 +- src/paypal/client.go | 3 +- src/paypal/order.go | 3 +- src/users/user.go | 2 +- static/donate.html | 205 +++++++++++++++++++++++++++++++++++++++++ static/register.html | 62 +++++++++++++ 14 files changed, 461 insertions(+), 41 deletions(-) create mode 100644 src/api/v1/register.go create mode 100644 src/discord/discord.go create mode 100644 static/donate.html create mode 100644 static/register.html diff --git a/go.mod b/go.mod index 4741519..48883f0 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/plutov/paypal/v3 v3.0.11 github.com/stretchr/testify v1.4.0 github.com/valyala/fasttemplate v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc // indirect + golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 golang.org/x/net v0.0.0-20191009170851-d66e71096ffb // indirect golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f // indirect golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index 17e5972..0aa01ed 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,8 @@ golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsu golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 h1:wCWoJcFExDgyYx2m2hpHgwz8W3+FPdfldvIgzqDIhyg= +golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= diff --git a/src/api/v1/donate.go b/src/api/v1/donate.go index c0840c4..7e517a6 100644 --- a/src/api/v1/donate.go +++ b/src/api/v1/donate.go @@ -1,10 +1,13 @@ package v1 import ( - "github.com/ImpactDevelopment/ImpactServer/src/jwt" + "log" + "net/http" + + "github.com/ImpactDevelopment/ImpactServer/src/database" "github.com/ImpactDevelopment/ImpactServer/src/paypal" + "github.com/google/uuid" "github.com/labstack/echo/v4" - "net/http" ) type ( @@ -12,8 +15,7 @@ type ( ID string `json:"orderID" form:"orderID" query:"orderID"` } donationResponse struct { - Amount int `json:"amount"` - Token string `json:"token,omitempty"` + Token string `json:"token"` } ) @@ -42,18 +44,23 @@ func afterDonation(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Error validating order").SetInternal(err) } - // This token can be used to register for an impact account, assuming amount is high enough - token := jwt.CreateDonationJWT(order) - if token == "" { - return echo.NewHTTPError(http.StatusInternalServerError, "Error creating donation token") + var token uuid.UUID + err = database.DB.QueryRow("INSERT INTO pending_donations(paypal_order_id, amount) VALUES ($1, $2) RETURNING token", order.ID, order.Total()).Scan(&token) + if err != nil { + log.Println(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Error saving pending donation").SetInternal(err) } - // TODO instead of returning a token, should we store it in the database and return a short token id instead? - // The jwt is rather long if users are planning to share it as a gift... - // Another option would be to compress it somehow maybe 🤔 + if order.Total() < 500 { + // if the donation is too small, save it + // maybe they make multiple that sum up to over 500 eventually, lets make a record of it idk + // just dont give em a registration token lol + log.Println("Donation with PayPal order ID", order.ID, "and total", order.Total(), "is too small. token would have been", token) + return c.JSON(http.StatusOK, donationResponse{ + Token: "thanks", + }) + } return c.JSON(http.StatusOK, donationResponse{ - Amount: order.Total(), - Token: token, - // TODO return expiry too? + Token: token.String(), }) } diff --git a/src/api/v1/register.go b/src/api/v1/register.go new file mode 100644 index 0000000..58d26a4 --- /dev/null +++ b/src/api/v1/register.go @@ -0,0 +1,94 @@ +package v1 + +import ( + "log" + "net/http" + "strings" + + "github.com/ImpactDevelopment/ImpactServer/src/database" + "github.com/ImpactDevelopment/ImpactServer/src/discord" + "github.com/ImpactDevelopment/ImpactServer/src/util" + "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" +) + +type registrationCheck struct { + Token string `json:"token" form:"token" query:"token"` +} + +type registration struct { + Token string `json:"token" form:"token" query:"token"` + Discord string `json:"discord" form:"discord" query:"discord"` + Mcuuid string `json:"mcuuid" form:"mcuuid" query:"mcuuid"` + Email string `json:"email" form:"email" query:"email"` + Password string `json:"password" form:"password" query:"password"` +} + +func checkToken(c echo.Context) error { + body := ®istrationCheck{} + err := c.Bind(body) + if err != nil { + return err + } + if body.Token == "" { + return echo.NewHTTPError(http.StatusBadRequest, "token missing") + } + var createdAt int64 + err = database.DB.QueryRow("SELECT created_at FROM pending_donations WHERE token = $1 AND NOT used", body.Token).Scan(&createdAt) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid token") + } + return c.String(200, "true") +} + +func registerWithToken(c echo.Context) error { + body := ®istration{} + err := c.Bind(body) + if err != nil { + return err + } + if body.Token == "" || body.Discord == "" || body.Mcuuid == "" || body.Email == "" || body.Password == "" { + return echo.NewHTTPError(http.StatusBadRequest, "empty field(s)") + } + var createdAt int64 + err = database.DB.QueryRow("SELECT created_at FROM pending_donations WHERE token = $1 AND NOT used", body.Token).Scan(&createdAt) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid token") + } + log.Println(body) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + if !discord.CheckServerMembership(body.Discord) { + return echo.NewHTTPError(http.StatusBadRequest, "join our discord first lol") + } + + req, err := util.GetRequest("https://api.mojang.com/user/profiles/" + strings.Replace(body.Mcuuid, "-", "", -1) + "/names") + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "bad mc uuid") + } + resp, err := req.Do() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "bad mc uuid") + } + if resp.Code() != 200 { + return echo.NewHTTPError(http.StatusBadRequest, "bad mc uuid") + } + _, err = database.DB.Exec("UPDATE pending_donations SET used = true WHERE token = $1", body.Token) + if err != nil { + log.Println(err) + return err + } + _, err = database.DB.Exec("INSERT INTO users(email, password_hash, mc_uuid, discord_id) VALUES ($1, $2, $3, $4)", body.Email, hashedPassword, body.Mcuuid, body.Discord) + if err != nil { + log.Println(err) + return err + } + err = discord.GiveDonator(body.Discord) + if err != nil { + log.Println(err) + return err + } + return c.String(200, "SUCCESS") +} diff --git a/src/api/v1/server.go b/src/api/v1/server.go index 8af70b5..683c48a 100644 --- a/src/api/v1/server.go +++ b/src/api/v1/server.go @@ -1,9 +1,10 @@ package v1 import ( - "github.com/ImpactDevelopment/ImpactServer/src/jwt" "net/http" + "github.com/ImpactDevelopment/ImpactServer/src/jwt" + "github.com/ImpactDevelopment/ImpactServer/src/middleware" "github.com/labstack/echo/v4" ) @@ -22,6 +23,8 @@ func API(api *echo.Group) { api.Match([]string{http.MethodGet, http.MethodPost}, "/login/minecraft", jwt.MinecraftLoginHandler, middleware.NoCache()) api.Match([]string{http.MethodGet, http.MethodPost}, "/login/discord", jwt.DiscordLoginHandler, middleware.NoCache()) api.Match([]string{http.MethodGet, http.MethodPost}, "/paypal/afterpayment", afterDonation, middleware.NoCache()) + api.Match([]string{http.MethodGet, http.MethodPost}, "/checktoken", checkToken, middleware.NoCache()) + api.Match([]string{http.MethodGet, http.MethodPost}, "/register/token", registerWithToken, middleware.NoCache()) api.GET("/emailtest", emailTest, middleware.NoCache()) api.GET("/premiumcheck", premiumCheck, middleware.NoCache()) api.GET("/integration/futureclient/masonlist", futureIntegration, middleware.NoCache()) diff --git a/src/database/schema.go b/src/database/schema.go index 22d7d6c..4e21173 100644 --- a/src/database/schema.go +++ b/src/database/schema.go @@ -20,6 +20,21 @@ func createTables() error { return err } + _, err = DB.Exec(` + CREATE TABLE IF NOT EXISTS pending_donations ( + token UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), + created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT, -- unix seconds + paypal_order_id TEXT, -- can be null in case we want to make a "gift card" with no paypal order id attached + amount INTEGER, -- can be null for the same reason + + used BOOL NOT NULL DEFAULT FALSE + ); + `) + if err != nil { + log.Println("Unable to create pending_donations table") + return err + } + _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS users ( user_id UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), diff --git a/src/discord/discord.go b/src/discord/discord.go new file mode 100644 index 0000000..2a70fa8 --- /dev/null +++ b/src/discord/discord.go @@ -0,0 +1,48 @@ +package discord + +import ( + "fmt" + "os" + + "github.com/bwmarrin/discordgo" +) + +var discord *discordgo.Session + +var guildID string +var donatorRole string + +func init() { + token := os.Getenv("DISCORD_BOT_TOKEN") + if token == "" { + fmt.Println("WARNING: No discord bot token, will not be able to grant donator role!") + return + } + guildID = os.Getenv("DISCORD_GUILD_ID") + donatorRole = os.Getenv("DISCORD_DONATOR_ROLE_ID") + if guildID == "" || donatorRole == "" { + fmt.Println("WARNING: Discord info is bad") + return + } + var err error + discord, err = discordgo.New("Bot " + token) + if err != nil { + panic(err) + } + user, err := discord.User("@me") + if err != nil { + panic(err) + } + + myselfID := user.ID + fmt.Println("I am", myselfID) +} + +func GiveDonator(discordID string) error { + return discord.GuildMemberRoleAdd(guildID, discordID, donatorRole) +} + +func CheckServerMembership(discordID string) bool { + member, err := discord.GuildMember(guildID, discordID) + return err == nil && member != nil +} diff --git a/src/jwt/jwt.go b/src/jwt/jwt.go index 5d34844..c15fdd2 100644 --- a/src/jwt/jwt.go +++ b/src/jwt/jwt.go @@ -3,12 +3,12 @@ package jwt import ( "crypto/rsa" "fmt" - "github.com/ImpactDevelopment/ImpactServer/src/paypal" - "github.com/labstack/echo/v4" "net/http" "os" "time" + "github.com/labstack/echo/v4" + "github.com/ImpactDevelopment/ImpactServer/src/users" "github.com/ImpactDevelopment/ImpactServer/src/util" "github.com/gbrlsnchs/jwt/v3" @@ -58,25 +58,6 @@ func init() { rs512 = jwt.NewRS512(jwt.RSAPrivateKey(key), jwt.RSAPublicKey(&key.PublicKey)) } -// CreateDonationJWT returns a jwt token for a paypal order which can then be used -// to register for an Impact Account. -func CreateDonationJWT(order *paypal.Order) string { - now := time.Now() - - return createJWT(donationJWT{ - Payload: jwt.Payload{ - Issuer: jwtIssuerURL, - Subject: "", - Audience: jwt.Audience{"impact_account"}, - ExpirationTime: jwt.NumericDate(now.Add(90 * 24 * time.Hour)), - NotBefore: jwt.NumericDate(now), - IssuedAt: jwt.NumericDate(now), - }, - OrderID: order.ID, - Amount: order.Total(), - }) -} - // CreateUserJWT returns a jwt token for the user with the subject (if not empty). // The client can then use this to verify that the user has authenticated // with a valid Impact server by checking the signature and issuer. diff --git a/src/jwt/minecraft.go b/src/jwt/minecraft.go index a5024cf..f656757 100644 --- a/src/jwt/minecraft.go +++ b/src/jwt/minecraft.go @@ -1,11 +1,12 @@ package jwt import ( + "net/http" + "github.com/ImpactDevelopment/ImpactServer/src/database" "github.com/ImpactDevelopment/ImpactServer/src/util" "github.com/google/uuid" "github.com/labstack/echo/v4" - "net/http" ) type minecraftRequest struct { diff --git a/src/paypal/client.go b/src/paypal/client.go index d4bd04a..935389b 100644 --- a/src/paypal/client.go +++ b/src/paypal/client.go @@ -2,8 +2,9 @@ package paypal import ( "fmt" - "github.com/plutov/paypal/v3" "os" + + "github.com/plutov/paypal/v3" ) var client *paypal.Client diff --git a/src/paypal/order.go b/src/paypal/order.go index 924df0f..a6c7020 100644 --- a/src/paypal/order.go +++ b/src/paypal/order.go @@ -3,9 +3,10 @@ package paypal import ( "errors" "fmt" - "github.com/plutov/paypal/v3" "strconv" "strings" + + "github.com/plutov/paypal/v3" ) type Order struct { diff --git a/src/users/user.go b/src/users/user.go index 7c971f8..dc7309b 100644 --- a/src/users/user.go +++ b/src/users/user.go @@ -5,7 +5,7 @@ import ( ) type User struct { - ID uuid.UUID + ID uuid.UUID Email string MinecraftID *uuid.UUID DiscordID string diff --git a/static/donate.html b/static/donate.html new file mode 100644 index 0000000..cfa144a --- /dev/null +++ b/static/donate.html @@ -0,0 +1,205 @@ + + + + + Donate to impact + + + + + + + + + + + + Donations of any size are appreciated! The minimum donation to receive premium features is $5. +
+
+
+
+
+
+
+ $ + + +
+
+ +
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/static/register.html b/static/register.html new file mode 100644 index 0000000..45033f5 --- /dev/null +++ b/static/register.html @@ -0,0 +1,62 @@ + + + + + Register + + + + + + + + + + + + +
+ Checking token... +
+ +

+ + + + \ No newline at end of file