2022-05-02 08:19:37 -07:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
2022-05-17 13:35:26 -07:00
|
|
|
"os"
|
2022-05-02 08:19:37 -07:00
|
|
|
|
2022-06-17 06:18:44 -07:00
|
|
|
"codeberg.org/u1f320/pronouns.cc/backend/db"
|
2022-05-14 07:52:08 -07:00
|
|
|
"codeberg.org/u1f320/pronouns.cc/backend/log"
|
|
|
|
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
2022-05-04 07:27:16 -07:00
|
|
|
"emperror.dev/errors"
|
2022-05-02 08:19:37 -07:00
|
|
|
"github.com/go-chi/chi/v5"
|
2022-05-04 07:27:16 -07:00
|
|
|
"github.com/go-chi/render"
|
2022-06-17 06:18:44 -07:00
|
|
|
"github.com/rs/xid"
|
2022-05-02 08:19:37 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
type Server struct {
|
|
|
|
*server.Server
|
2022-05-17 13:35:26 -07:00
|
|
|
|
|
|
|
RequireInvite bool
|
2022-05-02 08:19:37 -07:00
|
|
|
}
|
|
|
|
|
2022-06-17 06:18:44 -07:00
|
|
|
type userResponse struct {
|
|
|
|
ID xid.ID `json:"id"`
|
2022-11-20 16:24:23 -08:00
|
|
|
Username string `json:"name"`
|
2022-06-17 06:18:44 -07:00
|
|
|
DisplayName *string `json:"display_name"`
|
|
|
|
Bio *string `json:"bio"`
|
2022-09-20 03:55:00 -07:00
|
|
|
AvatarURLs []string `json:"avatar_urls"`
|
2022-06-17 06:18:44 -07:00
|
|
|
Links []string `json:"links"`
|
|
|
|
|
|
|
|
Discord *string `json:"discord"`
|
|
|
|
DiscordUsername *string `json:"discord_username"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func dbUserToUserResponse(u db.User) *userResponse {
|
|
|
|
return &userResponse{
|
|
|
|
ID: u.ID,
|
|
|
|
Username: u.Username,
|
|
|
|
DisplayName: u.DisplayName,
|
|
|
|
Bio: u.Bio,
|
2022-09-20 03:55:00 -07:00
|
|
|
AvatarURLs: u.AvatarURLs,
|
2022-06-17 06:18:44 -07:00
|
|
|
Links: u.Links,
|
|
|
|
Discord: u.Discord,
|
|
|
|
DiscordUsername: u.DiscordUsername,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-02 08:19:37 -07:00
|
|
|
func Mount(srv *server.Server, r chi.Router) {
|
2022-05-17 13:35:26 -07:00
|
|
|
s := &Server{
|
|
|
|
Server: srv,
|
|
|
|
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
|
|
|
}
|
2022-05-02 08:19:37 -07:00
|
|
|
|
2022-05-04 07:27:16 -07:00
|
|
|
r.Route("/auth", func(r chi.Router) {
|
2022-05-17 13:35:26 -07:00
|
|
|
// check if username is taken
|
|
|
|
r.Get("/username", server.WrapHandler(s.usernameTaken))
|
|
|
|
|
2022-05-04 07:27:16 -07:00
|
|
|
// generate csrf token, returns all supported OAuth provider URLs
|
2022-05-12 07:41:32 -07:00
|
|
|
r.Post("/urls", server.WrapHandler(s.oauthURLs))
|
2022-05-02 08:19:37 -07:00
|
|
|
|
2022-05-04 07:27:16 -07:00
|
|
|
r.Route("/discord", func(r chi.Router) {
|
|
|
|
// takes code + state, validates it, returns token OR discord signup ticket
|
2022-05-12 07:41:32 -07:00
|
|
|
r.Post("/callback", server.WrapHandler(s.discordCallback))
|
2022-05-04 07:27:16 -07:00
|
|
|
// takes discord signup ticket to register account
|
2022-11-17 17:17:27 -08:00
|
|
|
r.Post("/signup", server.WrapHandler(s.discordSignup))
|
2022-05-02 08:19:37 -07:00
|
|
|
})
|
2022-11-18 06:27:52 -08:00
|
|
|
|
|
|
|
// invite routes
|
|
|
|
r.With(server.MustAuth).Get("/invites", server.WrapHandler(s.getInvites))
|
|
|
|
r.With(server.MustAuth).Post("/invites", server.WrapHandler(s.createInvite))
|
2022-12-31 15:34:38 -08:00
|
|
|
|
|
|
|
// tokens
|
|
|
|
r.With(server.MustAuth).Get("/tokens", server.WrapHandler(s.getTokens))
|
|
|
|
r.With(server.MustAuth).Post("/tokens", server.WrapHandler(s.createToken))
|
|
|
|
r.With(server.MustAuth).Delete("/tokens", server.WrapHandler(s.deleteToken))
|
2022-05-02 08:19:37 -07:00
|
|
|
})
|
|
|
|
}
|
2022-05-04 07:27:16 -07:00
|
|
|
|
|
|
|
type oauthURLsRequest struct {
|
2022-05-12 07:41:32 -07:00
|
|
|
CallbackDomain string `json:"callback_domain"`
|
2022-05-04 07:27:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type oauthURLsResponse struct {
|
|
|
|
Discord string `json:"discord"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) oauthURLs(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
req, err := Decode[oauthURLsRequest](r)
|
|
|
|
if err != nil {
|
2022-05-12 07:41:32 -07:00
|
|
|
log.Error(err)
|
|
|
|
|
2022-05-04 07:27:16 -07:00
|
|
|
return server.APIError{Code: server.ErrBadRequest}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate CSRF state
|
|
|
|
state, err := s.setCSRFState(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "setting CSRF state")
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy Discord config and set redirect url
|
|
|
|
discordCfg := discordOAuthConfig
|
2022-05-12 07:41:32 -07:00
|
|
|
discordCfg.RedirectURL = req.CallbackDomain + "/login/discord"
|
2022-05-04 07:27:16 -07:00
|
|
|
|
|
|
|
render.JSON(w, r, oauthURLsResponse{
|
|
|
|
Discord: discordCfg.AuthCodeURL(state),
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
2022-05-17 13:35:26 -07:00
|
|
|
|
|
|
|
func (s *Server) usernameTaken(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
type Response struct {
|
|
|
|
Valid bool `json:"valid"`
|
|
|
|
Taken bool `json:"taken"`
|
|
|
|
}
|
|
|
|
|
|
|
|
name := r.FormValue("username")
|
|
|
|
if name == "" {
|
|
|
|
render.JSON(w, r, Response{
|
|
|
|
Valid: false,
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
valid, taken, err := s.DB.UsernameTaken(r.Context(), name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
render.JSON(w, r, Response{
|
|
|
|
Valid: valid,
|
|
|
|
Taken: taken,
|
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|