201 lines
5.4 KiB
Go
201 lines
5.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"arimelody.me/arimelody.me/discord"
|
|
"arimelody.me/arimelody.me/global"
|
|
)
|
|
|
|
type (
|
|
Session struct {
|
|
UserID string
|
|
Token string
|
|
Expires int64
|
|
}
|
|
)
|
|
|
|
const TOKEN_LENGTH = 64
|
|
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
// TODO: consider relying *entirely* on env vars instead of hard-coded fallbacks
|
|
var ADMIN_ID_DISCORD = func() string {
|
|
envvar := os.Getenv("DISCORD_ADMIN_ID")
|
|
if envvar != "" {
|
|
return envvar
|
|
} else {
|
|
return "356210742200107009"
|
|
}
|
|
}()
|
|
|
|
var sessions []*Session
|
|
|
|
func CreateSession(UserID string) Session {
|
|
return Session{
|
|
UserID: UserID,
|
|
Token: string(generateToken()),
|
|
Expires: time.Now().Add(24 * time.Hour).Unix(),
|
|
}
|
|
}
|
|
|
|
func Handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("hello /admin!"))
|
|
}))
|
|
mux.Handle("/callback", global.HTTPLog(OAuthCallbackHandler()))
|
|
mux.Handle("/login", global.HTTPLog(LoginHandler()))
|
|
mux.Handle("/verify", global.HTTPLog(AuthorisedHandler(VerifyHandler())))
|
|
mux.Handle("/logout", global.HTTPLog(AuthorisedHandler(LogoutHandler())))
|
|
|
|
return mux
|
|
}
|
|
|
|
func AuthorisedHandler(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
|
cookie, err := r.Cookie("token")
|
|
if err != nil {
|
|
w.WriteHeader(401)
|
|
w.Write([]byte("Unauthorized"))
|
|
return
|
|
}
|
|
auth = cookie.Value
|
|
}
|
|
auth = auth[7:]
|
|
|
|
var session *Session
|
|
for _, s := range sessions {
|
|
if s.Expires < time.Now().Unix() {
|
|
// expired session. remove it from the list!
|
|
new_sessions := []*Session{}
|
|
for _, ns := range sessions {
|
|
if ns.Token == s.Token {
|
|
continue
|
|
}
|
|
new_sessions = append(new_sessions, ns)
|
|
}
|
|
continue
|
|
}
|
|
if s.Token == auth {
|
|
session = s
|
|
break
|
|
}
|
|
}
|
|
|
|
if session == nil {
|
|
w.WriteHeader(401)
|
|
w.Write([]byte("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), "token", session.Token)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func LoginHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
code := r.URL.Query().Get("code")
|
|
|
|
if code == "" {
|
|
w.Header().Add("Location", discord.REDIRECT_URI)
|
|
w.WriteHeader(307)
|
|
return
|
|
}
|
|
|
|
auth_token, err := discord.GetOAuthTokenFromCode(code)
|
|
if err != nil {
|
|
fmt.Printf("Failed to retrieve discord access token: %s\n", err)
|
|
w.WriteHeader(500)
|
|
w.Write([]byte("Internal server error"))
|
|
return
|
|
}
|
|
|
|
discord_user, err := discord.GetDiscordUserFromAuth(auth_token)
|
|
if err != nil {
|
|
fmt.Printf("Failed to retrieve discord user information: %s\n", err)
|
|
w.WriteHeader(500)
|
|
w.Write([]byte("Internal server error"))
|
|
return
|
|
}
|
|
|
|
if discord_user.Id != ADMIN_ID_DISCORD {
|
|
// TODO: unauthorized user. revoke the token
|
|
w.WriteHeader(401)
|
|
w.Write([]byte("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
// login success!
|
|
session := CreateSession(discord_user.Username)
|
|
sessions = append(sessions, &session)
|
|
|
|
cookie := http.Cookie{}
|
|
cookie.Name = "token"
|
|
cookie.Value = session.Token
|
|
cookie.Expires = time.Now().Add(24 * time.Hour)
|
|
// cookie.Secure = true
|
|
cookie.HttpOnly = true
|
|
cookie.Path = "/"
|
|
http.SetCookie(w, &cookie)
|
|
|
|
w.WriteHeader(200)
|
|
w.Write([]byte(session.Token))
|
|
})
|
|
}
|
|
|
|
func LogoutHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
token := r.Context().Value("token").(string)
|
|
|
|
if token == "" {
|
|
w.WriteHeader(401)
|
|
return
|
|
}
|
|
|
|
// remove this session from the list
|
|
sessions = func (token string) []*Session {
|
|
new_sessions := []*Session{}
|
|
for _, session := range sessions {
|
|
new_sessions = append(new_sessions, session)
|
|
}
|
|
return new_sessions
|
|
}(token)
|
|
|
|
w.WriteHeader(200)
|
|
})
|
|
}
|
|
|
|
func OAuthCallbackHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
})
|
|
}
|
|
|
|
func VerifyHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// this is an authorised endpoint, so you *must* supply a valid token
|
|
// before accessing this route.
|
|
w.WriteHeader(200)
|
|
})
|
|
}
|
|
|
|
func generateToken() string {
|
|
var token []byte
|
|
|
|
for i := 0; i < TOKEN_LENGTH; i++ {
|
|
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))])
|
|
}
|
|
|
|
return string(token)
|
|
}
|