Merge development

Development
This commit is contained in:
Jesper Bak Handskemager 2022-09-21 22:45:20 +02:00 committed by GitHub
commit 0c31f46b3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 296 additions and 45 deletions

View File

@ -1,5 +1,15 @@
# CS:GO Hub backend # CS:GO Hub backend
Use with [csgo-hub-discord](https://github.com/jesperbakhandskemager/csgo-hub-discord)
## About
This is the backend, which works with Steam's OpenID to gather their Steam ID and convert it to the corresponding CS:GO friend code.
It functions with the [CS:GO Hub Discord bot](https://github.com/jesperbakhandskemager/csgo-hub-discord).
A user can request a token from the bot by issuing the `/link-steam` command in any server where the bot is present or in the bot's DM's.
The bot takes note of the Discord Id, and sends it along in the token request (the endpoint can only be accessed from localhost for security).
The bot responds to the user with a formatted link to the `steam.csgohub.xyz` site appended by their token, and the backend gathers various informations from both the database along with data from Discord's API.
Once a user is linked, anyone can issue the `/show-team` command in any channel the server owner permits and the bot will reply with a list of friend codes for any linked users in the same voice channel.
## Database ## Database
You need the following tables in MySQL You need the following tables in MySQL
@ -8,7 +18,7 @@ You need the following tables in MySQL
CREATE TABLE CREATE TABLE
`tokens` ( `tokens` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`discord_id` varchar(18) COLLATE utf8mb4_unicode_ci NOT NULL, `discord_id` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL, `token` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 17 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci ) ENGINE = InnoDB AUTO_INCREMENT = 17 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci
@ -16,7 +26,7 @@ CREATE TABLE
`users` ( `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(), `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`discord_id` varchar(18) COLLATE utf8mb4_unicode_ci NOT NULL, `discord_id` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL,
`friend_code` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT 'NULL', `friend_code` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT 'NULL',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 6 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci ) ENGINE = InnoDB AUTO_INCREMENT = 6 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci

62
assets/checkmark.css Normal file
View File

@ -0,0 +1,62 @@
svg {
width: 10rem;
height: 10rem;
display: block;
margin: 40px auto 0;
}
.path {
stroke-dasharray: 1000;
stroke-dashoffset: 0;
}
.path.circle {
-webkit-animation: dash 0.9s ease-in-out;
animation: dash 0.9s ease-in-out;
}
.path.line {
stroke-dashoffset: 1000;
-webkit-animation: dash 0.9s 0.35s ease-in-out forwards;
animation: dash 0.9s 0.35s ease-in-out forwards;
}
.path.check {
stroke-dashoffset: -100;
-webkit-animation: dash-check 0.9s 0.35s ease-in-out forwards;
animation: dash-check 0.9s 0.35s ease-in-out forwards;
}
p.success {
color: #73af55;
}
p.error {
color: #d06079;
}
@-webkit-keyframes dash {
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes dash {
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
}
@-webkit-keyframes dash-check {
0% {
stroke-dashoffset: -100;
}
100% {
stroke-dashoffset: 900;
}
}
@keyframes dash-check {
0% {
stroke-dashoffset: -100;
}
100% {
stroke-dashoffset: 900;
}
}

113
assets/style.css Normal file
View File

@ -0,0 +1,113 @@
.steam-login,
body {
-webkit-text-size-adjust: 100%
}
.navbar a,
.steam-login {
text-decoration: inherit
}
.footer,
.login,
.steam-login,
li a {
text-align: center
}
.name {
text-align: center;
text-decoration: inherit;
font-weight: 500;
}
.name:hover {
color: #535bf2;
}
body {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, .87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
}
h1 {
font-size: 3.2em;
line-height: 1.1
}
img {
scale: 1.2
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden
}
li .name {
display: block;
color: #fff;
padding: 14px 16px;
text-decoration: none
}
.navbar {
margin-top: 1.5rem
}
.navbar img {
border-radius: 50%;
scale: 1.0;
margin-right: 1.5rem
}
.steam-login {
vertical-align: 5rem;
tab-size: 4;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
box-sizing: border-box;
border: 1px solid #e5e7eb;
display: inline-block;
align-items: center;
border-radius: .375rem;
background-color: #1b2838;
color: #fff;
padding: .75rem 1.25rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 75ms
}
.steam-login:hover {
background-color: #4b5563
}
.steam-login svg {
vertical-align: -.3em;
margin-right: .3rem;
-webkit-filter: invert(100%);
filter: invert(100%)
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #181a1b;
color: #cecece
}

Binary file not shown.

View File

@ -1,32 +0,0 @@
<html>
<head>
<title>Steam OpenID</title>
</head>
<style>.login,li a{text-align:center}body{font-family:Inter,Avenir,Helvetica,Arial,sans-serif;font-size:16px;line-height:24px;font-weight:400;color-scheme:light dark;color:rgba(255,255,255,.87);background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}.login{margin:0;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}img{scale:1.2}ul{list-style-type:none;margin:0;padding:0;overflow:hidden}li a{display:block;color:#fff;padding:14px 16px;text-decoration:none}.navbar img{border-radius:50%;scale:1.0}</style>
<body>
{{if .DiscordName}}
<div class="navbar">
<ul>
<li style="float:right"><img alt="avatar" src="{{.DiscordAvatar}}" width="50" height="50px"></li>
<li style="float:right"><a class="active" href="#">{{.DiscordName}}</a></li>
</ul>
</div>
<div class="login">
<h1>Authenticate Steam</h1>
<p><b>Link {{.DiscordName}} with your Steam account</b></p>
<br>
<a href="/discover">
<img alt="Sign in through Steam"
src="http://steamcommunity-a.akamaihd.net/public/images/signinthroughsteam/sits_large_noborder.png" />
</a>
<div class="login">
{{else if .user}}
<p>{{.user}} Linked to your Discord account</p>
{{else}}
<p>An error occured</p>
{{end}}
</div>
</body>
</html>

View File

@ -147,7 +147,7 @@ func CreateToken(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
discord := vars["discord"] discord := vars["discord"]
if len(discord) != 18 { if len(discord) > 21 {
fmt.Fprintf(w, "Bad request") fmt.Fprintf(w, "Bad request")
return return
} }
@ -218,9 +218,8 @@ func main() {
router.HandleFunc("/discover", discoverHandler) router.HandleFunc("/discover", discoverHandler)
router.HandleFunc("/openidcallback", callbackHandler) router.HandleFunc("/openidcallback", callbackHandler)
router.HandleFunc("/{token}", indexHandler) router.HandleFunc("/{token}", indexHandler)
// router.HandleFunc("/api/v1/user/{id}", ReturnSingleUser).Methods("GET") router.PathPrefix("/static/").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./assets"))))
router.HandleFunc("/api/v1/users", GetMultipleUsers) router.HandleFunc("/api/v1/users", GetMultipleUsers)
//router.HandleFunc("/api/v1/user", CreateUser).Methods("POST")
router.HandleFunc("/api/v1/token/{discord}", CreateToken) router.HandleFunc("/api/v1/token/{discord}", CreateToken)
http.ListenAndServe(port, router) http.ListenAndServe(port, router)
} }

35
sites/authenticate.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
<title>CS:GO Hub</title>
</head>
<body>
<div class="navbar">
<ul>
<li style="float:right"><img alt="avatar" src="{{.DiscordAvatar}}" width="50" height="50px"></li>
<li style="float:right"><span class="name"><b>{{.DiscordName}}</b></span></li>
</ul>
</div>
<div class="login">
<h1>Authenticate Steam</h1>
<p><b>Link {{.DiscordName}} with your Steam account</b></p>
<br>
<a alt="Sign in through Steam" class="steam-login" href="/discover">
<svg width="1.7rem" height="1.7rem" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path
d="M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z" />
</svg>
<span>Login with Steam</span>
</a>
<br>
</div>
<div class="footer">
<p><i>This site is not associated with Valve Corp.</i></p>
</div>
</body>
</html>

30
sites/success.html Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/checkmark.css">
<title>CS:GO Hub</title>
</head>
<body>
<div class="navbar">
<ul>
<li style="float:right"><img alt="avatar" src="{{.DiscordAvatar}}" width="50" height="50px"></li>
<li style="float:right"><span class="name"><b>{{.DiscordName}}</b></span></li>
</ul>
</div>
<div class="login">
<h1>{{.Status}}</h1>
<br>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2">
<circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/>
<polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/>
</svg>
</div>
<div class="footer">
<p><i>This site is not associated with Valve Corp.</i></p>
</div>
</body>
</html>

View File

@ -16,9 +16,8 @@ import (
"github.com/yohcop/openid-go" "github.com/yohcop/openid-go"
) )
// Load the templates once var authenticateTmpl = template.Must(template.ParseFiles("./sites/authenticate.html"))
var templateDir = "./" var successTmpl = template.Must(template.ParseFiles("./sites/success.html"))
var indexTemplate = template.Must(template.ParseFiles(templateDir + "index.html"))
// NoOpDiscoveryCache implements the DiscoveryCache interface and doesn't cache anything. // NoOpDiscoveryCache implements the DiscoveryCache interface and doesn't cache anything.
// For a simple website, I'm not sure you need a cache. // For a simple website, I'm not sure you need a cache.
@ -39,6 +38,11 @@ type IndexStruct struct {
DiscordName string `json:"DiscordName"` DiscordName string `json:"DiscordName"`
DiscordAvatar string `json:"DiscordAvatar"` DiscordAvatar string `json:"DiscordAvatar"`
} }
type SuccessStruct struct {
DiscordName string `json:"DiscordName"`
DiscordAvatar string `json:"DiscordAvatar"`
Status string `json:"Status"`
}
type DiscordUser struct { type DiscordUser struct {
Id string `json:"id"` Id string `json:"id"`
@ -93,7 +97,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
expiration := time.Now().Add(time.Hour) expiration := time.Now().Add(time.Hour)
cookie := http.Cookie{Name: "token", Value: token, Expires: expiration} cookie := http.Cookie{Name: "token", Value: token, Expires: expiration}
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
indexTemplate.Execute(w, tmpl) authenticateTmpl.Execute(w, tmpl)
} }
// discoverHandler calls the Steam openid API and redirects to steam for login. // discoverHandler calls the Steam openid API and redirects to steam for login.
@ -118,6 +122,7 @@ func ClearToken(token string) error {
// callbackHandler handles the response back from Steam. It verifies the callback and then renders // callbackHandler handles the response back from Steam. It verifies the callback and then renders
// the index template with the logged in user's id. // the index template with the logged in user's id.
func callbackHandler(w http.ResponseWriter, r *http.Request) { func callbackHandler(w http.ResponseWriter, r *http.Request) {
var tmpl SuccessStruct
fullURL := "http://" + domain + r.URL.String() fullURL := "http://" + domain + r.URL.String()
id, err := openid.Verify(fullURL, discoveryCache, nonceStore) id, err := openid.Verify(fullURL, discoveryCache, nonceStore)
@ -150,6 +155,31 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) {
} }
ClearToken(token) ClearToken(token)
req, _ := http.NewRequest("GET", "https://discord.com/api/v9/users/"+discordId, nil)
req.Header.Add("Authorization", bearer)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Println("Error on response.\n[ERROR] -", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Error while reading the response bytes:", err)
}
var discord DiscordUser
json.Unmarshal(body, &discord)
var discordAvatarURL string
if discord.Avatar == "" {
discordAvatarURL = "https://csgohub.xyz/assets/empty-avatar.png"
} else {
discordAvatarURL = "https://cdn.discordapp.com/avatars/" + discord.Id + "/" + discord.Avatar + ".png?size=100"
}
tmpl.DiscordName = discord.Username
tmpl.DiscordAvatar = discordAvatarURL
var checkId, checkFriend string var checkId, checkFriend string
query = `SELECT discord_id, friend_code FROM users where discord_id = ?` query = `SELECT discord_id, friend_code FROM users where discord_id = ?`
err = db.QueryRow(query, discordId).Scan(&checkId, &checkFriend) err = db.QueryRow(query, discordId).Scan(&checkId, &checkFriend)
@ -161,8 +191,10 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Bad request") fmt.Fprintf(w, "Bad request")
} }
log.Print(err) log.Print(err)
w.WriteHeader(http.StatusCreated) // w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, "Account link updated") // fmt.Fprintf(w, "Account link updated")
tmpl.Status = "Account link updated"
successTmpl.Execute(w, tmpl)
return return
} }
@ -171,10 +203,12 @@ func callbackHandler(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Bad request") fmt.Fprintf(w, "Bad request")
return
} }
tmpl.Status = "Account linked"
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
data["user"] = id data["user"] = id
indexTemplate.Execute(w, data) successTmpl.Execute(w, tmpl)
} }
} }