diff --git a/aid/config.go b/aid/config.go index a9b1898..102f948 100644 --- a/aid/config.go +++ b/aid/config.go @@ -24,6 +24,7 @@ type CS struct { API struct { Host string Port string + FrontendPort string } JWT struct { Secret string @@ -91,6 +92,11 @@ func LoadConfig(file []byte) { panic("API Port is empty") } + Config.API.FrontendPort = cfg.Section("api").Key("frontend_port").String() + if Config.API.FrontendPort == "" { + Config.API.FrontendPort = Config.API.Port + } + Config.JWT.Secret = cfg.Section("jwt").Key("secret").String() if Config.JWT.Secret == "" { panic("JWT Secret is empty") diff --git a/aid/fiber.go b/aid/fiber.go index c788a12..81ee0ea 100644 --- a/aid/fiber.go +++ b/aid/fiber.go @@ -4,6 +4,7 @@ import ( "time" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/logger" ) @@ -22,10 +23,8 @@ func FiberLimiter() fiber.Handler { } func FiberCors() fiber.Handler { - return func(c *fiber.Ctx) error { - c.Set("Access-Control-Allow-Origin", "*") - c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin, Accept, X-Requested-With") - return c.Next() - } + return cors.New(cors.Config{ + AllowOrigins: "*", + AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Requested-With", + }) } \ No newline at end of file diff --git a/default.config.ini b/default.config.ini index 876b42f..a70ac33 100644 --- a/default.config.ini +++ b/default.config.ini @@ -7,17 +7,17 @@ type="postgres" drop=false [discord] -; discord oauth2 client id +; discord id of the bot id="1234567890..." -; discord oauth2 client secret +; oauth2 client secret secret="abcdefg..." ; discord bot token token="OTK...." [output] ; level of logging -; info = everything -; time = only time taken +; info = backend logs +; time = backend logs + time taken for database queries ; prod = only errors level="info" diff --git a/handlers/discord.go b/handlers/discord.go index 4495da2..2c01234 100644 --- a/handlers/discord.go +++ b/handlers/discord.go @@ -1,12 +1,87 @@ package handlers import ( + "net/http" "net/url" + "time" "github.com/ectrc/snow/aid" + p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/storage" + + "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" ) func GetDiscordOAuthURL(c *fiber.Ctx) error { - return c.Status(200).SendString("https://discord.com/api/oauth2/authorize?client_id="+ aid.Config.Discord.ID +"&redirect_uri="+ url.QueryEscape(aid.Config.API.Host + aid.Config.API.Port +"/snow/discord/callback") + "&response_type=code&scope=identify") + code := c.Query("code") + if code == "" { + return c.Status(200).SendString("https://discord.com/oauth2/authorize?client_id="+ aid.Config.Discord.ID +"&redirect_uri="+ url.QueryEscape(aid.Config.API.Host + aid.Config.API.Port +"/snow/discord") + "&response_type=code&scope=identify") + } + + client := &http.Client{} + + oauthRequest, err := client.PostForm("https://discord.com/api/v10/oauth2/token", url.Values{ + "client_id": {aid.Config.Discord.ID}, + "client_secret": {aid.Config.Discord.Secret}, + "grant_type": {"authorization_code"}, + "code": {code}, + "redirect_uri": {aid.Config.API.Host + aid.Config.API.Port +"/snow/discord"}, + }) + if err != nil { + return c.Status(500).JSON(aid.JSON{"error":err.Error()}) + } + + var body struct { + AccessToken string `json:"access_token"` + RenewToken string `json:"refresh_token"` + } + err = json.NewDecoder(oauthRequest.Body).Decode(&body) + if err != nil { + return c.Status(500).JSON(aid.JSON{"error":err.Error()}) + } + + userRequest, err := http.NewRequest("GET", "https://discord.com/api/v10/users/@me", nil) + if err != nil { + return c.Status(500).JSON(aid.JSON{"error":err.Error()}) + } + userRequest.Header.Set("Authorization", "Bearer " + body.AccessToken) + + userResponse, err := client.Do(userRequest) + if err != nil { + return c.Status(500).JSON(aid.JSON{"error":err.Error()}) + } + + var user struct { + ID string `json:"id"` + Username string `json:"username"` + } + err = json.NewDecoder(userResponse.Body).Decode(&user) + if err != nil { + return c.Status(500).JSON(aid.JSON{"error":err.Error()}) + } + + person := p.FindByDiscord(user.ID) + if person == nil { + return c.Status(404).JSON(aid.ErrorNotFound) + } + + person.Discord.AccessToken = body.AccessToken + person.Discord.RefreshToken = body.RenewToken + storage.Repo.SaveDiscordPerson(person.Discord) + + access, err := aid.JWTSign(aid.JSON{ + "snow_id": person.ID, // custom + "frontend": true, + "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), + }) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) + } + + c.Cookie(&fiber.Cookie{ + Name: "access_token", + Value: access, + }) + return c.Redirect(aid.Config.API.Host + aid.Config.API.FrontendPort + "/attempt") } \ No newline at end of file diff --git a/main.go b/main.go index 54e269a..87591c9 100644 --- a/main.go +++ b/main.go @@ -117,9 +117,10 @@ func main() { discord := snow.Group("/discord") discord.Get("/", handlers.GetDiscordOAuthURL) - + player := snow.Group("/player") player.Use(handlers.FrontendMiddleware) + player.Post("/", handlers.AnyNoContent) player.Get("/locker", handlers.GetPlayerLocker) r.Hooks().OnListen(func(ld fiber.ListenData) error { diff --git a/person/cache.go b/person/cache.go index 4fe9b5c..63ff447 100644 --- a/person/cache.go +++ b/person/cache.go @@ -65,6 +65,20 @@ func (m *PersonsCache) GetPersonByDisplay(displayName string) *Person { return person } +func (m *PersonsCache) GetPersonByDiscordID(discordId string) *Person { + var person *Person + m.RangeEntry(func(key string, value *CacheEntry) bool { + if value.Entry.Discord.ID == discordId { + person = value.Entry + return false + } + + return true + }) + + return person +} + func (m *PersonsCache) SavePerson(p *Person) { m.Store(p.ID, &CacheEntry{ Entry: p, diff --git a/person/loadout.go b/person/loadout.go index 7c5c6bb..e91f9d5 100644 --- a/person/loadout.go +++ b/person/loadout.go @@ -149,7 +149,6 @@ func (l *Loadout) GetAttribute(attribute string) interface{} { } } - switch attribute { case "locker_name": return l.LockerName diff --git a/person/person.go b/person/person.go index c1d9ae7..8f5111e 100644 --- a/person/person.go +++ b/person/person.go @@ -15,6 +15,7 @@ type Person struct { Profile0Profile *Profile CollectionsProfile *Profile CreativeProfile *Profile + Discord *storage.DB_DiscordPerson } type Option struct { @@ -59,16 +60,34 @@ func FindByDisplay(displayName string) *Person { cache = NewPersonsCacheMutex() } + cachedPerson := cache.GetPersonByDisplay(displayName) + if cachedPerson != nil { + return cachedPerson + } + person := storage.Repo.GetPersonByDisplayFromDB(displayName) if person == nil { return nil } - cachedPerson := cache.GetPerson(person.ID) + return findHelper(person) +} + +func FindByDiscord(discordId string) *Person { + if cache == nil { + cache = NewPersonsCacheMutex() + } + + cachedPerson := cache.GetPersonByDiscordID(discordId) if cachedPerson != nil { return cachedPerson } + person := storage.Repo.GetPersonByDiscordIDFromDB(discordId) + if person == nil { + return nil + } + return findHelper(person) } @@ -122,6 +141,7 @@ func findHelper(databasePerson *storage.DB_Person) *Person { Profile0Profile: profile0, CollectionsProfile: collectionsProfile, CreativeProfile: creativeProfile, + Discord: &databasePerson.Discord, } cache.SavePerson(person) @@ -182,6 +202,8 @@ func (p *Person) ToDatabase() *storage.DB_Person { DisplayName: p.DisplayName, Profiles: []storage.DB_Profile{}, AccessKey: p.AccessKey, + Discord: *p.Discord, + DiscordID: p.Discord.ID, } profilesToConvert := map[string]*Profile{ @@ -260,5 +282,6 @@ func (p *Person) Snapshot() *PersonSnapshot { Profile0Profile: *p.Profile0Profile.Snapshot(), CollectionsProfile: *p.CollectionsProfile.Snapshot(), CreativeProfile: *p.CreativeProfile.Snapshot(), + Discord: *p.Discord, } } \ No newline at end of file diff --git a/person/snapshot.go b/person/snapshot.go index fe174bc..e00de9b 100644 --- a/person/snapshot.go +++ b/person/snapshot.go @@ -1,5 +1,7 @@ package person +import "github.com/ectrc/snow/storage" + type PersonSnapshot struct { ID string DisplayName string @@ -9,6 +11,7 @@ type PersonSnapshot struct { Profile0Profile ProfileSnapshot CollectionsProfile ProfileSnapshot CreativeProfile ProfileSnapshot + Discord storage.DB_DiscordPerson } type ProfileSnapshot struct { diff --git a/storage/postgres.go b/storage/postgres.go index 97c1f5c..a4f8c64 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -12,7 +12,7 @@ type PostgresStorage struct { } func NewPostgresStorage() *PostgresStorage { - l := logger.Default.LogMode(logger.Silent) + l := logger.Default if aid.Config.Output.Level == "time" { l = logger.Default.LogMode(logger.Info) } @@ -43,6 +43,8 @@ func (s *PostgresStorage) MigrateAll() { s.Migrate(&DB_Loot{}, "Loot") s.Migrate(&DB_VariantChannel{}, "Variants") s.Migrate(&DB_PAttribute{}, "Attributes") + s.Migrate(&DB_TemporaryCode{}, "Exchange") + s.Migrate(&DB_DiscordPerson{}, "Discord") } func (s *PostgresStorage) DropTables() { @@ -52,6 +54,7 @@ func (s *PostgresStorage) DropTables() { func (s *PostgresStorage) GetPerson(personId string) *DB_Person { var dbPerson DB_Person s.Postgres. + Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). Preload("Profiles.Items.Variants"). @@ -60,6 +63,7 @@ func (s *PostgresStorage) GetPerson(personId string) *DB_Person { Preload("Profiles.Items"). Preload("Profiles.Gifts"). Preload("Profiles.Quests"). + Preload("Discord"). Where("id = ?", personId). Find(&dbPerson) @@ -73,6 +77,7 @@ func (s *PostgresStorage) GetPerson(personId string) *DB_Person { func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person { var dbPerson DB_Person s.Postgres. + Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). Preload("Profiles.Items.Variants"). @@ -81,6 +86,7 @@ func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person { Preload("Profiles.Items"). Preload("Profiles.Gifts"). Preload("Profiles.Quests"). + Preload("Discord"). Where("display_name = ?", displayName). Find(&dbPerson) @@ -91,10 +97,22 @@ func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person { return &dbPerson } +func (s *PostgresStorage) GetPersonByDiscordID(discorId string) *DB_Person { + var discordEntry DB_DiscordPerson + s.Postgres.Model(&DB_DiscordPerson{}).Where("id = ?", discorId).Find(&discordEntry) + + if discordEntry.ID == "" { + return nil + } + + return s.GetPerson(discordEntry.PersonID) +} + func (s *PostgresStorage) GetAllPersons() []*DB_Person { var dbPersons []*DB_Person s.Postgres. + Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). Preload("Profiles.Items.Variants"). @@ -103,6 +121,7 @@ func (s *PostgresStorage) GetAllPersons() []*DB_Person { Preload("Profiles.Items"). Preload("Profiles.Gifts"). Preload("Profiles.Quests"). + Preload("Discord"). Find(&dbPersons) return dbPersons @@ -178,4 +197,20 @@ func (s *PostgresStorage) SaveLoadout(loadout *DB_Loadout) { func (s *PostgresStorage) DeleteLoadout(loadoutId string) { s.Postgres.Delete(&DB_Loadout{}, "id = ?", loadoutId) +} + +func (s *PostgresStorage) SaveTemporaryCode(code *DB_TemporaryCode) { + s.Postgres.Save(code) +} + +func (s *PostgresStorage) DeleteTemporaryCode(codeId string) { + s.Postgres.Delete(&DB_TemporaryCode{}, "id = ?", codeId) +} + +func (s *PostgresStorage) SaveDiscordPerson(discordPerson *DB_DiscordPerson) { + s.Postgres.Save(discordPerson) +} + +func (s *PostgresStorage) DeleteDiscordPerson(discordPersonId string) { + s.Postgres.Delete(&DB_DiscordPerson{}, "id = ?", discordPersonId) } \ No newline at end of file diff --git a/storage/storage.go b/storage/storage.go index cf7af74..dc54196 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -9,6 +9,7 @@ type Storage interface { GetPerson(personId string) *DB_Person GetPersonByDisplay(displayName string) *DB_Person + GetPersonByDiscordID(discordId string) *DB_Person GetAllPersons() []*DB_Person SavePerson(person *DB_Person) @@ -35,6 +36,12 @@ type Storage interface { SaveLoadout(loadout *DB_Loadout) DeleteLoadout(loadoutId string) + + SaveTemporaryCode(code *DB_TemporaryCode) + DeleteTemporaryCode(codeId string) + + SaveDiscordPerson(person *DB_DiscordPerson) + DeleteDiscordPerson(personId string) } type Repository struct { @@ -65,6 +72,15 @@ func (r *Repository) GetPersonByDisplayFromDB(displayName string) *DB_Person { return nil } +func (r *Repository) GetPersonByDiscordIDFromDB(discordId string) *DB_Person { + storagePerson := r.Storage.GetPersonByDiscordID(discordId) + if storagePerson != nil { + return storagePerson + } + + return nil +} + func (r *Repository) GetAllPersons() []*DB_Person { return r.Storage.GetAllPersons() } @@ -135,4 +151,20 @@ func (r *Repository) SaveLoadout(loadout *DB_Loadout) { func (r *Repository) DeleteLoadout(loadoutId string) { r.Storage.DeleteLoadout(loadoutId) +} + +func (r *Repository) SaveTemporaryCode(code *DB_TemporaryCode) { + r.Storage.SaveTemporaryCode(code) +} + +func (r *Repository) DeleteTemporaryCode(codeId string) { + r.Storage.DeleteTemporaryCode(codeId) +} + +func (r *Repository) SaveDiscordPerson(person *DB_DiscordPerson) { + r.Storage.SaveDiscordPerson(person) +} + +func (r *Repository) DeleteDiscordPerson(personId string) { + r.Storage.DeleteDiscordPerson(personId) } \ No newline at end of file diff --git a/storage/tables.go b/storage/tables.go index 5db8854..e5f2805 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -11,6 +11,8 @@ type DB_Person struct { DisplayName string AccessKey string Profiles []DB_Profile `gorm:"foreignkey:PersonID"` + Discord DB_DiscordPerson `gorm:"foreignkey:PersonID"` + DiscordID string } func (DB_Person) TableName() string { @@ -26,7 +28,7 @@ type DB_Profile struct { Attributes []DB_PAttribute `gorm:"foreignkey:ProfileID"` Loadouts []DB_Loadout `gorm:"foreignkey:ProfileID"` Type string - Revision int + Revision int } func (DB_Profile) TableName() string { @@ -133,4 +135,27 @@ type DB_Loot struct { func (DB_Loot) TableName() string { return "Loot" +} + +type DB_TemporaryCode struct { + ID string `gorm:"primary_key"` + Code string + ExpiresAt int64 + PersonID string +} + +func (DB_TemporaryCode) TableName() string { + return "Exchange" +} + +type DB_DiscordPerson struct { + ID string `gorm:"primary_key"` + PersonID string + Username string + AccessToken string + RefreshToken string +} + +func (DB_DiscordPerson) TableName() string { + return "Discord" } \ No newline at end of file diff --git a/wiki/oauth.md b/wiki/oauth.md new file mode 100644 index 0000000..6c7ddc2 --- /dev/null +++ b/wiki/oauth.md @@ -0,0 +1,50 @@ +# Configure Discord OAuth + +## Getting your OAuth Credentials + +![Alt text](oauth1.png) + +![Alt text](oauth2.png) + +Part of the file `config.ini` should look like this: + +```ini +[discord] +; discord id of the bot +id="1234567890..." +; oauth2 client secret +secret="abcdefg..." +; discord bot token +token="OTK...." +``` + +Replace the values with your own, save and rebuild to apply the changes. + +## Setup the bot + +Add the correct redirects to your discord application: + +![Alt text](redirects.png) + +This will be from the `config.ini` file: + +```ini +[api] +port=":3000" +host="http://localhost" +``` + +Make sure to add `/snow/discord` to the end of the redirect url. + +## Inviting the bot + +Generate an invite link for the bot with the following permissions: + +![Alt text](scopes1.png) +![Alt text](scopes2.png) + +The invite link should look like this: + +```url +https://discord.com/api/oauth2/authorize?client_id=CLIENT_ID&permissions=34816&redirect_uri=CALLBACK_URL&scope=bot+applications.commands +``` diff --git a/wiki/oauth1.png b/wiki/oauth1.png new file mode 100644 index 0000000..bf01909 Binary files /dev/null and b/wiki/oauth1.png differ diff --git a/wiki/oauth2.png b/wiki/oauth2.png new file mode 100644 index 0000000..afe65cd Binary files /dev/null and b/wiki/oauth2.png differ diff --git a/wiki/redirects.png b/wiki/redirects.png new file mode 100644 index 0000000..7fb7b0d Binary files /dev/null and b/wiki/redirects.png differ diff --git a/wiki/scopes1.png b/wiki/scopes1.png new file mode 100644 index 0000000..98929b6 Binary files /dev/null and b/wiki/scopes1.png differ diff --git a/wiki/scopes2.png b/wiki/scopes2.png new file mode 100644 index 0000000..d096439 Binary files /dev/null and b/wiki/scopes2.png differ