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" var ADMIN_ID_DISCORD = func() string { envvar := os.Getenv("DISCORD_ADMIN_ID") if envvar == "" { fmt.Printf("DISCORD_ADMIN_ID was not provided. Admin login will be unavailable.\n") } return envvar }() 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(http.StatusOK) w.Write([]byte("hello /admin!")) })) mux.Handle("/callback", global.HTTPLog(OAuthCallbackHandler())) mux.Handle("/login", global.HTTPLog(LoginHandler())) mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler()))) mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler()))) return mux } func MustAuthorise(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // TEMPORARY ctx := context.WithValue(r.Context(), "role", "admin") next.ServeHTTP(w, r.WithContext(ctx)) return /* auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { auth = auth[7:] } else { cookie, err := r.Cookie("token") if err != nil { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } auth = cookie.Value } 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 { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), "role", "admin") next.ServeHTTP(w, r.WithContext(ctx)) */ }) } func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ADMIN_ID_DISCORD == "" { http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) return } code := r.URL.Query().Get("code") if code == "" { http.Redirect(w, r, discord.REDIRECT_URI, http.StatusTemporaryRedirect) return } auth_token, err := discord.GetOAuthTokenFromCode(code) if err != nil { fmt.Printf("Failed to retrieve discord access token: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } discord_user, err := discord.GetDiscordUserFromAuth(auth_token) if err != nil { fmt.Printf("Failed to retrieve discord user information: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if discord_user.Id != ADMIN_ID_DISCORD { // TODO: unauthorized user; revoke the token http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 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(http.StatusOK) 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 == "" { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 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(http.StatusOK) w.Write([]byte("OK")) }) } 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(http.StatusOK) w.Write([]byte("OK")) }) } 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) }