Add setup wiki for discord oauth + nearly finish web interface

This commit is contained in:
eccentric 2023-12-10 00:52:59 +00:00
parent 0dc1423b9d
commit 8e198dcdd7
18 changed files with 278 additions and 16 deletions

View File

@ -24,6 +24,7 @@ type CS struct {
API struct { API struct {
Host string Host string
Port string Port string
FrontendPort string
} }
JWT struct { JWT struct {
Secret string Secret string
@ -91,6 +92,11 @@ func LoadConfig(file []byte) {
panic("API Port is empty") 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() Config.JWT.Secret = cfg.Section("jwt").Key("secret").String()
if Config.JWT.Secret == "" { if Config.JWT.Secret == "" {
panic("JWT Secret is empty") panic("JWT Secret is empty")

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/gofiber/fiber/v2" "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/limiter"
"github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/logger"
) )
@ -22,10 +23,8 @@ func FiberLimiter() fiber.Handler {
} }
func FiberCors() fiber.Handler { func FiberCors() fiber.Handler {
return func(c *fiber.Ctx) error { return cors.New(cors.Config{
c.Set("Access-Control-Allow-Origin", "*") AllowOrigins: "*",
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Requested-With",
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin, Accept, X-Requested-With") })
return c.Next()
}
} }

View File

@ -7,17 +7,17 @@ type="postgres"
drop=false drop=false
[discord] [discord]
; discord oauth2 client id ; discord id of the bot
id="1234567890..." id="1234567890..."
; discord oauth2 client secret ; oauth2 client secret
secret="abcdefg..." secret="abcdefg..."
; discord bot token ; discord bot token
token="OTK...." token="OTK...."
[output] [output]
; level of logging ; level of logging
; info = everything ; info = backend logs
; time = only time taken ; time = backend logs + time taken for database queries
; prod = only errors ; prod = only errors
level="info" level="info"

View File

@ -1,12 +1,87 @@
package handlers package handlers
import ( import (
"net/http"
"net/url" "net/url"
"time"
"github.com/ectrc/snow/aid" "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" "github.com/gofiber/fiber/v2"
) )
func GetDiscordOAuthURL(c *fiber.Ctx) error { 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")
} }

View File

@ -117,9 +117,10 @@ func main() {
discord := snow.Group("/discord") discord := snow.Group("/discord")
discord.Get("/", handlers.GetDiscordOAuthURL) discord.Get("/", handlers.GetDiscordOAuthURL)
player := snow.Group("/player") player := snow.Group("/player")
player.Use(handlers.FrontendMiddleware) player.Use(handlers.FrontendMiddleware)
player.Post("/", handlers.AnyNoContent)
player.Get("/locker", handlers.GetPlayerLocker) player.Get("/locker", handlers.GetPlayerLocker)
r.Hooks().OnListen(func(ld fiber.ListenData) error { r.Hooks().OnListen(func(ld fiber.ListenData) error {

View File

@ -65,6 +65,20 @@ func (m *PersonsCache) GetPersonByDisplay(displayName string) *Person {
return 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) { func (m *PersonsCache) SavePerson(p *Person) {
m.Store(p.ID, &CacheEntry{ m.Store(p.ID, &CacheEntry{
Entry: p, Entry: p,

View File

@ -149,7 +149,6 @@ func (l *Loadout) GetAttribute(attribute string) interface{} {
} }
} }
switch attribute { switch attribute {
case "locker_name": case "locker_name":
return l.LockerName return l.LockerName

View File

@ -15,6 +15,7 @@ type Person struct {
Profile0Profile *Profile Profile0Profile *Profile
CollectionsProfile *Profile CollectionsProfile *Profile
CreativeProfile *Profile CreativeProfile *Profile
Discord *storage.DB_DiscordPerson
} }
type Option struct { type Option struct {
@ -59,16 +60,34 @@ func FindByDisplay(displayName string) *Person {
cache = NewPersonsCacheMutex() cache = NewPersonsCacheMutex()
} }
cachedPerson := cache.GetPersonByDisplay(displayName)
if cachedPerson != nil {
return cachedPerson
}
person := storage.Repo.GetPersonByDisplayFromDB(displayName) person := storage.Repo.GetPersonByDisplayFromDB(displayName)
if person == nil { if person == nil {
return 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 { if cachedPerson != nil {
return cachedPerson return cachedPerson
} }
person := storage.Repo.GetPersonByDiscordIDFromDB(discordId)
if person == nil {
return nil
}
return findHelper(person) return findHelper(person)
} }
@ -122,6 +141,7 @@ func findHelper(databasePerson *storage.DB_Person) *Person {
Profile0Profile: profile0, Profile0Profile: profile0,
CollectionsProfile: collectionsProfile, CollectionsProfile: collectionsProfile,
CreativeProfile: creativeProfile, CreativeProfile: creativeProfile,
Discord: &databasePerson.Discord,
} }
cache.SavePerson(person) cache.SavePerson(person)
@ -182,6 +202,8 @@ func (p *Person) ToDatabase() *storage.DB_Person {
DisplayName: p.DisplayName, DisplayName: p.DisplayName,
Profiles: []storage.DB_Profile{}, Profiles: []storage.DB_Profile{},
AccessKey: p.AccessKey, AccessKey: p.AccessKey,
Discord: *p.Discord,
DiscordID: p.Discord.ID,
} }
profilesToConvert := map[string]*Profile{ profilesToConvert := map[string]*Profile{
@ -260,5 +282,6 @@ func (p *Person) Snapshot() *PersonSnapshot {
Profile0Profile: *p.Profile0Profile.Snapshot(), Profile0Profile: *p.Profile0Profile.Snapshot(),
CollectionsProfile: *p.CollectionsProfile.Snapshot(), CollectionsProfile: *p.CollectionsProfile.Snapshot(),
CreativeProfile: *p.CreativeProfile.Snapshot(), CreativeProfile: *p.CreativeProfile.Snapshot(),
Discord: *p.Discord,
} }
} }

View File

@ -1,5 +1,7 @@
package person package person
import "github.com/ectrc/snow/storage"
type PersonSnapshot struct { type PersonSnapshot struct {
ID string ID string
DisplayName string DisplayName string
@ -9,6 +11,7 @@ type PersonSnapshot struct {
Profile0Profile ProfileSnapshot Profile0Profile ProfileSnapshot
CollectionsProfile ProfileSnapshot CollectionsProfile ProfileSnapshot
CreativeProfile ProfileSnapshot CreativeProfile ProfileSnapshot
Discord storage.DB_DiscordPerson
} }
type ProfileSnapshot struct { type ProfileSnapshot struct {

View File

@ -12,7 +12,7 @@ type PostgresStorage struct {
} }
func NewPostgresStorage() *PostgresStorage { func NewPostgresStorage() *PostgresStorage {
l := logger.Default.LogMode(logger.Silent) l := logger.Default
if aid.Config.Output.Level == "time" { if aid.Config.Output.Level == "time" {
l = logger.Default.LogMode(logger.Info) l = logger.Default.LogMode(logger.Info)
} }
@ -43,6 +43,8 @@ func (s *PostgresStorage) MigrateAll() {
s.Migrate(&DB_Loot{}, "Loot") s.Migrate(&DB_Loot{}, "Loot")
s.Migrate(&DB_VariantChannel{}, "Variants") s.Migrate(&DB_VariantChannel{}, "Variants")
s.Migrate(&DB_PAttribute{}, "Attributes") s.Migrate(&DB_PAttribute{}, "Attributes")
s.Migrate(&DB_TemporaryCode{}, "Exchange")
s.Migrate(&DB_DiscordPerson{}, "Discord")
} }
func (s *PostgresStorage) DropTables() { func (s *PostgresStorage) DropTables() {
@ -52,6 +54,7 @@ func (s *PostgresStorage) DropTables() {
func (s *PostgresStorage) GetPerson(personId string) *DB_Person { func (s *PostgresStorage) GetPerson(personId string) *DB_Person {
var dbPerson DB_Person var dbPerson DB_Person
s.Postgres. s.Postgres.
Model(&DB_Person{}).
Preload("Profiles"). Preload("Profiles").
Preload("Profiles.Loadouts"). Preload("Profiles.Loadouts").
Preload("Profiles.Items.Variants"). Preload("Profiles.Items.Variants").
@ -60,6 +63,7 @@ func (s *PostgresStorage) GetPerson(personId string) *DB_Person {
Preload("Profiles.Items"). Preload("Profiles.Items").
Preload("Profiles.Gifts"). Preload("Profiles.Gifts").
Preload("Profiles.Quests"). Preload("Profiles.Quests").
Preload("Discord").
Where("id = ?", personId). Where("id = ?", personId).
Find(&dbPerson) Find(&dbPerson)
@ -73,6 +77,7 @@ func (s *PostgresStorage) GetPerson(personId string) *DB_Person {
func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person { func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person {
var dbPerson DB_Person var dbPerson DB_Person
s.Postgres. s.Postgres.
Model(&DB_Person{}).
Preload("Profiles"). Preload("Profiles").
Preload("Profiles.Loadouts"). Preload("Profiles.Loadouts").
Preload("Profiles.Items.Variants"). Preload("Profiles.Items.Variants").
@ -81,6 +86,7 @@ func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person {
Preload("Profiles.Items"). Preload("Profiles.Items").
Preload("Profiles.Gifts"). Preload("Profiles.Gifts").
Preload("Profiles.Quests"). Preload("Profiles.Quests").
Preload("Discord").
Where("display_name = ?", displayName). Where("display_name = ?", displayName).
Find(&dbPerson) Find(&dbPerson)
@ -91,10 +97,22 @@ func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person {
return &dbPerson 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 { func (s *PostgresStorage) GetAllPersons() []*DB_Person {
var dbPersons []*DB_Person var dbPersons []*DB_Person
s.Postgres. s.Postgres.
Model(&DB_Person{}).
Preload("Profiles"). Preload("Profiles").
Preload("Profiles.Loadouts"). Preload("Profiles.Loadouts").
Preload("Profiles.Items.Variants"). Preload("Profiles.Items.Variants").
@ -103,6 +121,7 @@ func (s *PostgresStorage) GetAllPersons() []*DB_Person {
Preload("Profiles.Items"). Preload("Profiles.Items").
Preload("Profiles.Gifts"). Preload("Profiles.Gifts").
Preload("Profiles.Quests"). Preload("Profiles.Quests").
Preload("Discord").
Find(&dbPersons) Find(&dbPersons)
return dbPersons return dbPersons
@ -178,4 +197,20 @@ func (s *PostgresStorage) SaveLoadout(loadout *DB_Loadout) {
func (s *PostgresStorage) DeleteLoadout(loadoutId string) { func (s *PostgresStorage) DeleteLoadout(loadoutId string) {
s.Postgres.Delete(&DB_Loadout{}, "id = ?", loadoutId) 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)
} }

View File

@ -9,6 +9,7 @@ type Storage interface {
GetPerson(personId string) *DB_Person GetPerson(personId string) *DB_Person
GetPersonByDisplay(displayName string) *DB_Person GetPersonByDisplay(displayName string) *DB_Person
GetPersonByDiscordID(discordId string) *DB_Person
GetAllPersons() []*DB_Person GetAllPersons() []*DB_Person
SavePerson(person *DB_Person) SavePerson(person *DB_Person)
@ -35,6 +36,12 @@ type Storage interface {
SaveLoadout(loadout *DB_Loadout) SaveLoadout(loadout *DB_Loadout)
DeleteLoadout(loadoutId string) DeleteLoadout(loadoutId string)
SaveTemporaryCode(code *DB_TemporaryCode)
DeleteTemporaryCode(codeId string)
SaveDiscordPerson(person *DB_DiscordPerson)
DeleteDiscordPerson(personId string)
} }
type Repository struct { type Repository struct {
@ -65,6 +72,15 @@ func (r *Repository) GetPersonByDisplayFromDB(displayName string) *DB_Person {
return nil 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 { func (r *Repository) GetAllPersons() []*DB_Person {
return r.Storage.GetAllPersons() return r.Storage.GetAllPersons()
} }
@ -135,4 +151,20 @@ func (r *Repository) SaveLoadout(loadout *DB_Loadout) {
func (r *Repository) DeleteLoadout(loadoutId string) { func (r *Repository) DeleteLoadout(loadoutId string) {
r.Storage.DeleteLoadout(loadoutId) 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)
} }

View File

@ -11,6 +11,8 @@ type DB_Person struct {
DisplayName string DisplayName string
AccessKey string AccessKey string
Profiles []DB_Profile `gorm:"foreignkey:PersonID"` Profiles []DB_Profile `gorm:"foreignkey:PersonID"`
Discord DB_DiscordPerson `gorm:"foreignkey:PersonID"`
DiscordID string
} }
func (DB_Person) TableName() string { func (DB_Person) TableName() string {
@ -26,7 +28,7 @@ type DB_Profile struct {
Attributes []DB_PAttribute `gorm:"foreignkey:ProfileID"` Attributes []DB_PAttribute `gorm:"foreignkey:ProfileID"`
Loadouts []DB_Loadout `gorm:"foreignkey:ProfileID"` Loadouts []DB_Loadout `gorm:"foreignkey:ProfileID"`
Type string Type string
Revision int Revision int
} }
func (DB_Profile) TableName() string { func (DB_Profile) TableName() string {
@ -133,4 +135,27 @@ type DB_Loot struct {
func (DB_Loot) TableName() string { func (DB_Loot) TableName() string {
return "Loot" 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"
} }

50
wiki/oauth.md Normal file
View File

@ -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
```

BIN
wiki/oauth1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
wiki/oauth2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
wiki/redirects.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
wiki/scopes1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
wiki/scopes2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB