diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..e0ccc01
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,7 @@
+config:
+ MYSQL_DB: "cshub"
+ MYSQL_USER: "root"
+ MYSQL_PASS: ""
+ MYSQL_HOST: "127.0.0.1:3306"
+ DOMAIN: "steam.csgohub.xyz"
+ PORT: ":8383"
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1b9b794
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,13 @@
+module github.com/jesperbakhandskemager/csgo-hub-backend
+
+go 1.19
+
+require (
+ github.com/emily33901/go-csfriendcode v0.0.0-20200914202423-31b418e4b897
+ github.com/go-sql-driver/mysql v1.6.0
+ github.com/gorilla/mux v1.8.0
+ github.com/yohcop/openid-go v1.0.0
+ gopkg.in/yaml.v2 v2.4.0
+)
+
+require golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..eb00d23
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,16 @@
+github.com/emily33901/go-csfriendcode v0.0.0-20200914202423-31b418e4b897 h1:/2VV8RykOeIbKbFRaA37wvz3LylTjlwmqRPN1uL90Z4=
+github.com/emily33901/go-csfriendcode v0.0.0-20200914202423-31b418e4b897/go.mod h1:Fc5k4zUsP3edBqsb8AZdUE/S1ON4D0sVHI4Rcu2pA7Y=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/yohcop/openid-go v1.0.0 h1:EciJ7ZLETHR3wOtxBvKXx9RV6eyHZpCaSZ1inbBaUXE=
+github.com/yohcop/openid-go v1.0.0/go.mod h1:/408xiwkeItSPJZSTPF7+VtZxPkPrRRpRNK2vjGh6yI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..b22df5b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
Steam OpenID
+
+
+
+ {{if .user}}
+
{{.user}} Linked to your Discord account
+ {{else}}
+
Authenticate Steam
+
+
+
+ {{end}}
+ {{if .error}}
+
An error occured
+ {{end}}
+
+
+
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9c729d6
--- /dev/null
+++ b/main.go
@@ -0,0 +1,212 @@
+package main
+
+import (
+ "crypto/rand"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "math/big"
+ "net/http"
+ "os"
+
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/gorilla/mux"
+ "gopkg.in/yaml.v2"
+)
+
+var domain string
+var port string
+
+type YAMLFile struct {
+ Config Config `yaml:"config"`
+}
+
+type Config struct {
+ MYSQL_DB string `yaml:"MYSQL_DB"`
+ MYSQL_USER string `yaml:"MYSQL_USER"`
+ MYSQL_PASS string `yaml:"MYSQL_PASS"`
+ MYSQL_HOST string `yaml:"MYSQL_HOST"`
+ DOMAIN string `yaml:"DOMAIN"`
+ PORT string `yaml:"PORT"`
+}
+
+func ReadConfig() (*Config, error) {
+ config := &YAMLFile{}
+ cfgFile, err := os.ReadFile("./config.yaml")
+ if err != nil {
+ return nil, err
+ }
+ err = yaml.Unmarshal(cfgFile, config)
+ return &config.Config, err
+}
+
+type user struct {
+ Id int `json:"id"`
+ CreatedAt string `json:"created_at"`
+ DiscordId string `json:"discord_id"`
+ FriendCode string `json:"friend_code"`
+}
+
+func ReturnSingleUser(w http.ResponseWriter, r *http.Request) {
+ var u user
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ query := `SELECT id, created_at, discord_id, friend_code FROM users where discord_id = ?`
+ err := db.QueryRow(query, id).Scan(&u.Id, &u.CreatedAt, &u.DiscordId, &u.FriendCode)
+ if err != nil {
+ log.Print(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad Request")
+ return
+ }
+
+ json.NewEncoder(w).Encode(u)
+}
+
+func GetMultipleUsers(w http.ResponseWriter, r *http.Request) {
+ // Read body
+ b, err := io.ReadAll(r.Body)
+ defer r.Body.Close()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+
+ // Unmarshal
+ var getUsers []user
+ err = json.Unmarshal(b, &getUsers)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), 500)
+ return
+ }
+
+ var returnUsers []user
+
+ for _, us := range getUsers {
+ var u user
+ query := `SELECT id, created_at, discord_id, friend_code FROM users where discord_id = ?`
+ err := db.QueryRow(query, us.DiscordId).Scan(&u.Id, &u.CreatedAt, &u.DiscordId, &u.FriendCode)
+ if err != nil {
+ log.Print(err)
+ }
+ returnUsers = append(returnUsers, u)
+ }
+
+ json.NewEncoder(w).Encode(returnUsers)
+}
+
+func CreateUser(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Host != "localhost:8383" {
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprintf(w, "Unauthorized")
+ return
+ }
+ // Read body
+ b, err := io.ReadAll(r.Body)
+ defer r.Body.Close()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+
+ // Unmarshal
+ var msg user
+ err = json.Unmarshal(b, &msg)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), 500)
+ return
+ }
+
+ _, err = db.Exec(`INSERT INTO users(discord_id, friend_code) VALUES (?, ?)`, msg.DiscordId, msg.FriendCode)
+ if err != nil {
+ log.Println(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, "OK")
+}
+
+func CreateToken(w http.ResponseWriter, r *http.Request) {
+ if r.Host != "localhost:8383" {
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprintf(w, "Unauthorized")
+ return
+ }
+ token, err := GenerateRandomString(8)
+ if err != nil {
+ return
+ }
+
+ vars := mux.Vars(r)
+ discord := vars["discord"]
+ if len(discord) != 18 {
+ fmt.Fprintf(w, "Bad request")
+ return
+ }
+
+ _, err = db.Exec(`INSERT INTO tokens(discord_id, token) VALUES (?, ?)`, discord, token)
+ if err != nil {
+ log.Println(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ return
+ }
+
+ json.NewEncoder(w).Encode(token)
+}
+
+func GenerateRandomString(n int) (string, error) {
+ const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
+ ret := make([]byte, n)
+ for i := 0; i < n; i++ {
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
+ if err != nil {
+ return "", err
+ }
+ ret[i] = letters[num.Int64()]
+ }
+
+ return string(ret), nil
+}
+
+var db *sql.DB
+
+func main() {
+ var err error
+ config, err := ReadConfig()
+ if err != nil {
+ log.Fatal(err)
+ }
+ domain = config.DOMAIN
+ port = config.PORT
+
+ db, err = sql.Open("mysql", config.MYSQL_USER+":"+config.MYSQL_PASS+"@("+config.MYSQL_HOST+")/"+config.MYSQL_DB+"?parseTime=true")
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = db.Ping()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer db.Close()
+ log.Println("Database connection established")
+
+ router := mux.NewRouter()
+
+ router.HandleFunc("/", indexHandler)
+ router.HandleFunc("/discover", discoverHandler)
+ router.HandleFunc("/openidcallback", callbackHandler)
+ router.HandleFunc("/{token}", indexHandler)
+ // router.HandleFunc("/api/v1/user/{id}", ReturnSingleUser).Methods("GET")
+ router.HandleFunc("/api/v1/users", GetMultipleUsers)
+ //router.HandleFunc("/api/v1/user", CreateUser).Methods("POST")
+ router.HandleFunc("/api/v1/token/{discord}", CreateToken)
+ http.ListenAndServe(port, router)
+}
diff --git a/steam.go b/steam.go
new file mode 100644
index 0000000..8dd2c7d
--- /dev/null
+++ b/steam.go
@@ -0,0 +1,136 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/emily33901/go-csfriendcode"
+ "github.com/gorilla/mux"
+ "github.com/yohcop/openid-go"
+)
+
+// Load the templates once
+var templateDir = "./"
+var indexTemplate = template.Must(template.ParseFiles(templateDir + "index.html"))
+
+// NoOpDiscoveryCache implements the DiscoveryCache interface and doesn't cache anything.
+// For a simple website, I'm not sure you need a cache.
+type NoOpDiscoveryCache struct{}
+
+// Put is a no op.
+func (n *NoOpDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {}
+
+// Get always returns nil.
+func (n *NoOpDiscoveryCache) Get(id string) openid.DiscoveredInfo {
+ return nil
+}
+
+var nonceStore = openid.NewSimpleNonceStore()
+var discoveryCache = &NoOpDiscoveryCache{}
+
+// indexHandler serves up the index template with the "Sign in through STEAM" button.
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ token := vars["token"]
+ query := `SELECT token FROM tokens where token = BINARY ?`
+ err := db.QueryRow(query, token).Scan(&token)
+ if err != nil {
+ log.Print(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ return
+ }
+ log.Println(token)
+ expiration := time.Now().Add(time.Hour)
+ cookie := http.Cookie{Name: "token", Value: token, Expires: expiration}
+ http.SetCookie(w, &cookie)
+ indexTemplate.Execute(w, nil)
+}
+
+// discoverHandler calls the Steam openid API and redirects to steam for login.
+func discoverHandler(w http.ResponseWriter, r *http.Request) {
+ url, err := openid.RedirectURL(
+ "http://steamcommunity.com/openid",
+ "http://"+domain+"/openidcallback",
+ "http://"+domain+"/")
+
+ if err != nil {
+ log.Printf("Error creating redirect URL: %q\n", err)
+ } else {
+ http.Redirect(w, r, url, http.StatusSeeOther)
+ }
+}
+
+func ClearToken(token string) error {
+ _, err := db.Exec(`DELETE FROM tokens where token = ?`, token)
+ return err
+}
+
+// callbackHandler handles the response back from Steam. It verifies the callback and then renders
+// the index template with the logged in user's id.
+func callbackHandler(w http.ResponseWriter, r *http.Request) {
+ fullURL := "http://" + domain + r.URL.String()
+
+ id, err := openid.Verify(fullURL, discoveryCache, nonceStore)
+ if err != nil {
+ log.Printf("Error verifying: %q\n", err)
+ } else {
+ log.Printf("NonceStore: %+v\n", nonceStore)
+ data := make(map[string]string)
+ println(id)
+ steamId, err := strconv.Atoi(strings.Split(id, "/id/")[1])
+ if err != nil {
+ log.Println(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ return
+ }
+
+ friendCode := csfriendcode.Encode(uint64(steamId))
+ cookie, _ := r.Cookie("token")
+ token := cookie.Value
+ var discordId string
+
+ query := `SELECT discord_id FROM tokens where token = ?`
+ err = db.QueryRow(query, token).Scan(&discordId)
+ if err != nil {
+ log.Print(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ return
+ }
+ ClearToken(token)
+
+ var checkId, checkFriend string
+ query = `SELECT discord_id, friend_code FROM users where discord_id = ?`
+ err = db.QueryRow(query, discordId).Scan(&checkId, &checkFriend)
+ if err == nil {
+ _, err = db.Exec(`UPDATE users SET friend_code = ? WHERE discord_id = ?`, friendCode, discordId)
+ if err != nil {
+ log.Println(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ }
+ log.Print(err)
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, "Account link updated")
+ return
+ }
+
+ _, err = db.Exec(`INSERT INTO users(discord_id, friend_code) VALUES (?, ?)`, discordId, friendCode)
+ if err != nil {
+ log.Println(err)
+ w.WriteHeader(http.StatusBadRequest)
+ fmt.Fprintf(w, "Bad request")
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ data["user"] = id
+ indexTemplate.Execute(w, data)
+ }
+}