package transport import ( "context" "crypto/rand" "database/sql" "encoding/base64" "encoding/json" "errors" "fmt" "github.com/alexedwards/flow" "github.com/google/uuid" "log" "net/http" "whitelistmanager/auth" "whitelistmanager/invite" "whitelistmanager/minecraft" "whitelistmanager/store" ) type Handler struct { store store.Storer manager invite.InviteManager } type Handle interface { AuthMiddleware(next http.Handler) http.Handler Cors(next http.Handler) http.Handler CurrentUser(w http.ResponseWriter, r *http.Request) CreateInvite(w http.ResponseWriter, r *http.Request) GetInvite(w http.ResponseWriter, r *http.Request) DeleteInvite(w http.ResponseWriter, r *http.Request) AcceptInvite(w http.ResponseWriter, r *http.Request) AuthRedirect(w http.ResponseWriter, r *http.Request) AuthCallback(w http.ResponseWriter, r *http.Request) Server(w http.ResponseWriter, r *http.Request) Servers(w http.ResponseWriter, r *http.Request) CreateServer(w http.ResponseWriter, r *http.Request) DeleteServer(w http.ResponseWriter, r *http.Request) ServerInvites(w http.ResponseWriter, r *http.Request) InviteLog(w http.ResponseWriter, r *http.Request) } func New(store store.Storer) *Handler { im := invite.NewManager(store) return &Handler{ store: store, manager: im, } } func (h *Handler) Cors(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization") w.Header().Set("Access-Control-Allow-Methods", "*") next.ServeHTTP(w, r) }) } func (h *Handler) SessionAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session, err := r.Cookie("session") if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusForbidden) return } user, err := h.store.SessionUser(session.Value) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusForbidden) return } ctx := context.WithValue(r.Context(), "user", user) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } type invitePayload struct { Server string `json:"server"` Unlimited bool `json:"unlimited"` Uses int `json:"uses"` } func (h *Handler) CreateInvite(w http.ResponseWriter, r *http.Request) { var i invitePayload err := json.NewDecoder(r.Body).Decode(&i) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } user := r.Context().Value("user").(store.User) in, err := h.manager.Create(store.Invite{ Server: store.Server{Id: i.Server}, Unlimited: i.Unlimited, Uses: i.Uses, }, user) if err != nil { if err.Error() == invite.NotOwnerofServer { http.Error(w, err.Error(), http.StatusForbidden) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write([]byte(in)) } func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) { token := flow.Param(r.Context(), "id") in, err := h.store.GetInvite(token) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, "invalid invite", http.StatusNotFound) return } http.Error(w, err.Error(), http.StatusBadRequest) return } err = json.NewEncoder(w).Encode(in) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func (h *Handler) AcceptInvite(w http.ResponseWriter, r *http.Request) { token := flow.Param(r.Context(), "id") in, err := h.store.GetInvite(token) if err != nil { if errors.Is(err, sql.ErrNoRows) { http.Error(w, "invalid invite", http.StatusNotFound) return } http.Error(w, err.Error(), http.StatusBadRequest) return } remainingUse, err := h.manager.RemainingUses(in) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if remainingUse == 0 { http.Error(w, "no more uses remaining for invite", http.StatusForbidden) return } server, err := h.store.GetServer(in.Server.Id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } user := r.Context().Value("user").(store.User) log.Println(user.DisplayName) resp, err := minecraft.Whitelist(user.DisplayName, server) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if resp == fmt.Sprintf("Added %s to the whitelist", user.DisplayName) { err = h.store.LogInviteUse(user, in) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } w.Write([]byte(resp)) } func (h *Handler) AuthRedirect(w http.ResponseWriter, r *http.Request) { state := store.OauthState{ Id: uuid.New().String(), Origin: r.Header.Get("Referer"), State: "started", } err := h.store.SaveOauthState(state) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, auth.LoginUrl(state.Id), http.StatusTemporaryRedirect) } func (h *Handler) AuthCallback(w http.ResponseWriter, r *http.Request) { code := r.URL.Query()["code"][0] stateId := r.URL.Query()["state"][0] state, err := h.store.OauthState(stateId) if err != nil || state.State == "completed" { http.Error(w, err.Error(), http.StatusForbidden) return } user, err := auth.Authenticate(code) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } err = h.store.SaveUser(user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } sessToken, err := generateSessionToken() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: "session", Value: sessToken, HttpOnly: true, Path: "/", }) err = h.store.SaveSession(sessToken, user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } state.State = "completed" err = h.store.SaveOauthState(state) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, state.Origin, http.StatusTemporaryRedirect) } func generateSessionToken() (string, error) { b := make([]byte, 64) _, err := rand.Read(b) if err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), err } func (h *Handler) Servers(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(store.User) if r.Method == http.MethodGet { servers, err := h.store.GetUserServers(user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if len(servers) == 0 { w.WriteHeader(http.StatusNotFound) w.Write([]byte("[]")) return } json.NewEncoder(w).Encode(servers) return } var server store.Server err := json.NewDecoder(r.Body).Decode(&server) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } server.Id = uuid.New().String() server.Owner.Id = user.Id err = h.store.SaveServer(server) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, "created server %s", server.Id) } func (h *Handler) Server(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(store.User) serverId := flow.Param(r.Context(), "id") if r.Method == http.MethodGet { server, err := h.store.GetServer(serverId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if server.Owner.Id != user.Id { http.Error(w, "not owner of server", http.StatusForbidden) return } server.Rcon.Password = "" json.NewEncoder(w).Encode(server) return } if r.Method == http.MethodDelete { serverId := flow.Param(r.Context(), "id") user := r.Context().Value("user").(store.User) server, err := h.store.GetServer(serverId) if err != nil { http.Error(w, "no such server", http.StatusNotFound) return } if server.Owner.Id != user.Id { http.Error(w, "user not server owner", http.StatusForbidden) return } err = h.store.DeleteServer(server, user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write([]byte("deleted")) } } func (h *Handler) CurrentUser(w http.ResponseWriter, r *http.Request) { value := r.Context().Value("user") if value == nil { http.Error(w, "", http.StatusForbidden) return } user := value.(store.User) w.Write([]byte(user.DisplayName)) } func (h *Handler) ServerInvites(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(store.User) serverId := flow.Param(r.Context(), "id") server, err := h.store.GetServer(serverId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if server.Owner.Id != user.Id { http.Error(w, "not owner of server", http.StatusForbidden) return } serverInvites, err := h.store.ServerInvites(server) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if server.Owner.Id != user.Id { http.Error(w, "not owner of server", http.StatusForbidden) return } if len(serverInvites) == 0 { w.WriteHeader(http.StatusNotFound) w.Write([]byte("[]")) return } json.NewEncoder(w).Encode(serverInvites) return } func (h *Handler) InviteLog(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(store.User) inviteToken := flow.Param(r.Context(), "id") invite, err := h.store.GetInvite(inviteToken) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if invite.Creator.Id != user.Id { http.Error(w, "not owner of invite", http.StatusForbidden) return } logs, err := h.store.InviteLog(invite) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if len(logs) == 0 { w.WriteHeader(http.StatusNotFound) w.Write([]byte("[]")) return } json.NewEncoder(w).Encode(logs) } func (h *Handler) DeleteInvite(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(store.User) inviteToken := flow.Param(r.Context(), "id") invite, err := h.store.GetInvite(inviteToken) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if invite.Creator.Id != user.Id { http.Error(w, "not owner of invite", http.StatusForbidden) return } err = h.store.DeleteInvite(invite) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write([]byte("deleted")) }