Quellcode durchsuchen

add slack oauth, integrations routes

Anukul Sangwan vor 4 Jahren
Ursprung
Commit
bf6aa4512d

+ 32 - 0
api/server/handlers/handler.go

@@ -1,11 +1,14 @@
 package handlers
 
 import (
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 )
 
@@ -13,6 +16,7 @@ type PorterHandler interface {
 	Config() *config.Config
 	Repo() repository.Repository
 	HandleAPIError(w http.ResponseWriter, r *http.Request, err apierrors.RequestError)
+	PopulateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error
 }
 
 type PorterHandlerWriter interface {
@@ -71,3 +75,31 @@ func (d *DefaultPorterHandler) DecodeAndValidateNoWrite(r *http.Request, v inter
 func IgnoreAPIError(w http.ResponseWriter, r *http.Request, err apierrors.RequestError) {
 	return
 }
+
+func (d *DefaultPorterHandler) PopulateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error {
+	session, err := d.Config().Store.Get(r, d.Config().ServerConf.CookieName)
+
+	if err != nil {
+		return err
+	}
+
+	// need state parameter to validate when redirected
+	session.Values["state"] = state
+	session.Values["query_params"] = r.URL.RawQuery
+
+	if isProject {
+		project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+		if project == nil {
+			return fmt.Errorf("could not read project")
+		}
+
+		session.Values["project_id"] = project.ID
+	}
+
+	if err := session.Save(r, w); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 77 - 0
api/server/handlers/oauth_callback/slack.go

@@ -0,0 +1,77 @@
+package oauth_callback
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/internal/integrations/slack"
+)
+
+type OAuthCallbackSlackHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewOAuthCallbackSlackHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OAuthCallbackSlackHandler {
+	return &OAuthCallbackSlackHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *OAuthCallbackSlackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	session, err := p.Config().Store.Get(r, p.Config().ServerConf.CookieName)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if _, ok := session.Values["state"]; !ok {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if r.URL.Query().Get("state") != session.Values["state"] {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	token, err := p.Config().SlackConf.Exchange(context.TODO(), r.URL.Query().Get("code"))
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	slackInt, err := slack.TokenToSlackIntegration(token)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+	projID, _ := session.Values["project_id"].(uint)
+
+	slackInt.UserID = userID
+	slackInt.ProjectID = projID
+
+	if _, err = p.Repo().SlackIntegration().CreateSlackIntegration(slackInt); err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if session.Values["query_params"] != "" {
+		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	} else {
+		http.Redirect(w, r, "/dashboard", 302)
+	}
+}

+ 41 - 0
api/server/handlers/project_oauth/slack.go

@@ -0,0 +1,41 @@
+package project_oauth
+
+import (
+	"net/http"
+
+	"golang.org/x/oauth2"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/internal/oauth"
+)
+
+type ProjectOAuthSlackHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewProjectOAuthSlackHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ProjectOAuthSlackHandler {
+	return &ProjectOAuthSlackHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *ProjectOAuthSlackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	state := oauth.CreateRandomState()
+
+	if err := p.PopulateOAuthSession(w, r, state, true); err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// specify access type offline to get a refresh token
+	url := p.Config().SlackConf.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+	http.Redirect(w, r, url, 302)
+}

+ 50 - 0
api/server/handlers/slack_integration/delete.go

@@ -0,0 +1,50 @@
+package slack_integration
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type SlackIntegrationDelete struct {
+	handlers.PorterHandler
+}
+
+func NewSlackIntegrationDelete(
+	config *config.Config,
+) *SlackIntegrationDelete {
+	return &SlackIntegrationDelete{
+		PorterHandler: handlers.NewDefaultPorterHandler(config, nil, nil),
+	}
+}
+
+func (p *SlackIntegrationDelete) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	integrationID, _ := requestutils.GetURLParamUint(r, types.URLParamSlackIntegrationID)
+
+	slackInts, err := p.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	for _, slackInt := range slackInts {
+		if slackInt.ID == integrationID {
+			err = p.Repo().SlackIntegration().DeleteSlackIntegration(slackInt.ID)
+			if err != nil {
+				p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+			w.WriteHeader(http.StatusOK)
+			return
+		}
+	}
+
+	w.WriteHeader(http.StatusNotFound)
+}

+ 40 - 0
api/server/handlers/slack_integration/exists.go

@@ -0,0 +1,40 @@
+package slack_integration
+
+import (
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type SlackIntegrationExists struct {
+	handlers.PorterHandler
+}
+
+func NewSlackIntegrationExists(
+	config *config.Config,
+) *SlackIntegrationExists {
+	return &SlackIntegrationExists{
+		PorterHandler: handlers.NewDefaultPorterHandler(config, nil, nil),
+	}
+}
+
+func (p *SlackIntegrationExists) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	slackInts, err := p.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if len(slackInts) != 0 {
+		w.WriteHeader(http.StatusOK)
+	} else {
+		w.WriteHeader(http.StatusNotFound)
+	}
+}

+ 44 - 0
api/server/handlers/slack_integration/list.go

@@ -0,0 +1,44 @@
+package slack_integration
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type SlackIntegrationListHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewSlackIntegrationListHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *SlackIntegrationListHandler {
+	return &SlackIntegrationListHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *SlackIntegrationListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	slackInts, err := p.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make(types.ListSlackIntegrationsResponse, 0)
+
+	for _, slackInt := range slackInts {
+		res = append(res, slackInt.ToSlackIntegraionType())
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 54 - 0
api/server/router/oauth_callback.go

@@ -0,0 +1,54 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/oauth_callback"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewOAuthCallbackRegisterer(children ...*Registerer) *Registerer {
+	return &Registerer{
+		GetRoutes: GetOAuthCallbackRoutes,
+		Children:  children,
+	}
+}
+
+func GetOAuthCallbackRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*Registerer,
+) []*Route {
+	relPath := "/oauth"
+
+	routes := make([]*Route, 0)
+
+	// GET /api/oauth/slack/callback -> oauth_callback.NewOAuthCallbackSlackHandler
+	slackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/slack/callback",
+			},
+		},
+	)
+
+	slackHandler := oauth_callback.NewOAuthCallbackSlackHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: slackEndpoint,
+		Handler:  slackHandler,
+		Router:   r,
+	})
+
+	return routes
+}

+ 85 - 0
api/server/router/project_oauth.go

@@ -0,0 +1,85 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+
+	"github.com/porter-dev/porter/api/server/handlers/project_oauth"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewProjectOAuthScopedRegisterer(children ...*Registerer) *Registerer {
+	return &Registerer{
+		GetRoutes: GetProjectOAuthScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetProjectOAuthScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*Registerer,
+) []*Route {
+	routes, projPath := getProjectOAuthRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getProjectOAuthRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*Route, *types.Path) {
+	relPath := "/oauth"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	routes := make([]*Route, 0)
+
+	// GET /api/projects/{project_id}/oauth/slack -> project_integration.NewProjectOAuthSlackHandler
+	slackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/slack",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	slackHandler := project_oauth.NewProjectOAuthSlackHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: slackEndpoint,
+		Handler:  slackHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 26 - 2
api/server/router/router.go

@@ -19,7 +19,9 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	r.Use(ContentTypeJSON)
 
 	endpointFactory := shared.NewAPIObjectEndpointFactory(config)
+
 	baseRegisterer := NewBaseRegisterer()
+	oauthCallbackRegisterer := NewOAuthCallbackRegisterer()
 
 	releaseRegisterer := NewReleaseScopedRegisterer()
 	namespaceRegisterer := NewNamespaceScopedRegisterer(releaseRegisterer)
@@ -30,6 +32,8 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	helmRepoRegisterer := NewHelmRepoScopedRegisterer()
 	inviteRegisterer := NewInviteScopedRegisterer()
 	projectIntegrationRegisterer := NewProjectIntegrationScopedRegisterer()
+	projectOAuthRegisterer := NewProjectOAuthScopedRegisterer()
+	slackIntegrationRegisterer := NewSlackIntegrationScopedRegisterer()
 	projRegisterer := NewProjectScopedRegisterer(
 		clusterRegisterer,
 		registryRegisterer,
@@ -38,6 +42,8 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 		gitInstallationRegisterer,
 		infraRegisterer,
 		projectIntegrationRegisterer,
+		projectOAuthRegisterer,
+		slackIntegrationRegisterer,
 	)
 	userRegisterer := NewUserScopedRegisterer(projRegisterer)
 
@@ -51,6 +57,15 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 			endpointFactory,
 		)
 
+		oauthCallbackRoutes := oauthCallbackRegisterer.GetRoutes(
+			r,
+			config,
+			&types.Path{
+				RelativePath: "",
+			},
+			endpointFactory,
+		)
+
 		userRoutes := userRegisterer.GetRoutes(
 			r,
 			config,
@@ -61,9 +76,18 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 			userRegisterer.Children...,
 		)
 
-		routes := append(baseRoutes, userRoutes...)
+		routes := [][]*Route{
+			baseRoutes,
+			userRoutes,
+			oauthCallbackRoutes,
+		}
+
+		var allRoutes []*Route
+		for _, r := range routes {
+			allRoutes = append(allRoutes, r...)
+		}
 
-		registerRoutes(config, routes)
+		registerRoutes(config, allRoutes)
 	})
 
 	return r

+ 131 - 0
api/server/router/slack_integration.go

@@ -0,0 +1,131 @@
+package router
+
+import (
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/api/server/handlers/slack_integration"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewSlackIntegrationScopedRegisterer(children ...*Registerer) *Registerer {
+	return &Registerer{
+		GetRoutes: GetSlackIntegrationScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetSlackIntegrationScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*Registerer,
+) []*Route {
+	routes, projPath := getSlackIntegrationRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getSlackIntegrationRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*Route, *types.Path) {
+	relPath := "/slack_integrations"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	routes := make([]*Route, 0)
+
+	// GET /api/projects/{project_id}/slack_integrations -> slack_integration.NewListHandler
+	listEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listHandler := slack_integration.NewSlackIntegrationListHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listEndpoint,
+		Handler:  listHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/slack_integrations/exists -> slack_integration.NewListHandler
+	existsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/exists",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	existsHandler := slack_integration.NewSlackIntegrationExists(config)
+
+	routes = append(routes, &Route{
+		Endpoint: existsEndpoint,
+		Handler:  existsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/slack_integrations/exists -> slack_integration.NewListHandler
+	deleteEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{slack_integration_id}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	deleteHandler := slack_integration.NewSlackIntegrationDelete(config)
+
+	routes = append(routes, &Route{
+		Endpoint: deleteEndpoint,
+		Handler:  deleteHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 3 - 0
api/server/shared/config/config.go

@@ -58,6 +58,9 @@ type Config struct {
 	// GithubAppConf is the configuration for a Github App OAuth client
 	GithubAppConf *oauth.GithubAppConf
 
+	// SlackConf is the configuration for a Slack OAuth client
+	SlackConf *oauth2.Config
+
 	// WSUpgrader upgrades HTTP connections to websocket connections
 	WSUpgrader *websocket.Upgrader
 

+ 12 - 0
api/server/shared/config/loader/loader.go

@@ -136,6 +136,18 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		}
 	}
 
+	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {
+		res.SlackConf = oauth.NewSlackClient(&oauth.Config{
+			ClientID:     sc.SlackClientID,
+			ClientSecret: sc.SlackClientSecret,
+			Scopes: []string{
+				"incoming-webhook",
+				"team:read",
+			},
+			BaseURL: sc.ServerURL,
+		})
+	}
+
 	res.WSUpgrader = &websocket.Upgrader{
 		ReadBufferSize:  1024,
 		WriteBufferSize: 1024,

+ 28 - 0
api/types/slack_integration.go

@@ -0,0 +1,28 @@
+package types
+
+const (
+	URLParamSlackIntegrationID = "slack_integration_id"
+)
+
+type SlackIntegration struct {
+	ID uint `json:"id"`
+
+	ProjectID uint `json:"project_id"`
+
+	// The ID for the Slack team
+	TeamID string `json:"team_id"`
+
+	// The name of the Slack team
+	TeamName string `json:"team_name"`
+
+	// The icon url for the Slack team
+	TeamIconURL string `json:"team_icon_url"`
+
+	// The channel name that the Slack app is installed in
+	Channel string `json:"channel"`
+
+	// The URL for configuring the workspace app instance
+	ConfigurationURL string `json:"configuration_url"`
+}
+
+type ListSlackIntegrationsResponse []*SlackIntegration

+ 1 - 1
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -121,7 +121,7 @@ const IntegrationCategories: React.FC<Props> = (props) => {
                   ),
               });
             } else {
-              window.location.href = `/api/oauth/projects/${currentProject.id}/slack`;
+              window.location.href = `/api/projects/${currentProject.id}/oauth/slack`;
             }
           }}
         >

+ 5 - 5
docs/developing/backend-refactor-status.md

@@ -26,8 +26,8 @@
 | <li>- [ ] `GET /api/oauth/login/google`                                                                                     |             |                 |             |                  |
 | <li>- [ ] `GET /api/oauth/projects/{project_id}/digitalocean`                                                               |             |                 |             |                  |
 | <li>- [ ] `GET /api/oauth/projects/{project_id}/github`                                                                     |             |                 |             |                  |
-| <li>- [ ] `GET /api/oauth/projects/{project_id}/slack`                                                                      |             |                 |             |                  |
-| <li>- [ ] `GET /api/oauth/slack/callback`                                                                                   |             |                 |             |                  |
+| <li>- [x] `GET /api/oauth/projects/{project_id}/slack`                                                                      | AS          | yes             |             | yes              |
+| <li>- [x] `GET /api/oauth/slack/callback`                                                                                   | AS          |                 |             | yes              |
 | <li>- [x] `POST /api/password/reset/finalize`                                                                               | AB          |                 |             | yes              |
 | <li>- [x] `POST /api/password/reset/initiate`                                                                               | AB          |                 |             | yes              |
 | <li>- [x] `POST /api/password/reset/verify`                                                                                 | AB          |                 |             | yes              |
@@ -139,9 +139,9 @@
 | <li>- [x] `GET /api/projects/{project_id}/roles`                                                                            | AS          |                 |             | yes              |
 | <li>- [x] `POST /api/projects/{project_id}/roles/{user_id}`                                                                 | AS          | yes             |             | yes              |
 | <li>- [x] `DELETE /api/projects/{project_id}/roles/{user_id}`                                                               | AS          | yes             |             | yes              |
-| <li>- [ ] `GET /api/projects/{project_id}/slack_integrations`                                                               |             |                 |             |                  |
-| <li>- [ ] `GET /api/projects/{project_id}/slack_integrations/exists`                                                        |             |                 |             |                  |
-| <li>- [ ] `DELETE /api/projects/{project_id}/slack_integrations/{slack_integration_id}`                                     |             |                 |             |                  |
+| <li>- [x] `GET /api/projects/{project_id}/slack_integrations`                                                               | AS          |                 |             | yes              |
+| <li>- [x] `GET /api/projects/{project_id}/slack_integrations/exists`                                                        | AS          |                 |             | yes              |
+| <li>- [x] `DELETE /api/projects/{project_id}/slack_integrations/{slack_integration_id}`                                     | AS          |                 |             | yes              |
 | <li>- [ ] `GET /api/readyz`                                                                                                 |             |                 |             |                  |
 | <li>- [X] `GET /api/templates`                                                                                              | AB          |                 |             |                  |
 | <li>- [X] `GET /api/templates/upgrade_notes/{name}/{version}`                                                               | AB          | yes             |             |                  |

+ 2 - 27
internal/models/integrations/slack.go

@@ -46,33 +46,8 @@ type SlackIntegration struct {
 	Webhook []byte
 }
 
-// SlackIntegrationExternal is an external SlackIntegration to be shared over
-// rest
-type SlackIntegrationExternal struct {
-	ID uint `json:"id"`
-
-	ProjectID uint `json:"project_id"`
-
-	// The ID for the Slack team
-	TeamID string `json:"team_id"`
-
-	// The name of the Slack team
-	TeamName string `json:"team_name"`
-
-	// The icon url for the Slack team
-	TeamIconURL string `json:"team_icon_url"`
-
-	// The channel name that the Slack app is installed in
-	Channel string `json:"channel"`
-
-	// The URL for configuring the workspace app instance
-	ConfigurationURL string `json:"configuration_url"`
-}
-
-// Externalize generates an external SlackIntegration to be shared over
-// rest
-func (s *SlackIntegration) Externalize() *SlackIntegrationExternal {
-	return &SlackIntegrationExternal{
+func (s *SlackIntegration) ToSlackIntegraionType() *types.SlackIntegration {
+	return &types.SlackIntegration{
 		ID:               s.ID,
 		ProjectID:        s.ProjectID,
 		TeamID:           s.TeamID,