Embded frontend inside go binary

This will simplify the self hosting of the platform.

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-09 15:30:28 +01:00
parent 2c82dbad39
commit 123a538d82
40 changed files with 527 additions and 262 deletions

View File

@@ -0,0 +1,47 @@
ALTER TABLE organizations ADD COLUMN tenant_id TEXT;
UPDATE organizations SET tenant_id = id;
ALTER TABLE organizations ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE frameworks ADD COLUMN tenant_id TEXT;
UPDATE frameworks f SET tenant_id = f.organization_id;
ALTER TABLE frameworks ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE controls ADD COLUMN tenant_id TEXT;
UPDATE controls c SET tenant_id = (SELECT organization_id FROM frameworks f WHERE f.id = c.framework_id);
ALTER TABLE controls ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE tasks ADD COLUMN tenant_id TEXT;
UPDATE tasks t SET tenant_id = (SELECT c.tenant_id FROM controls_tasks ct JOIN controls c ON ct.control_id = c.id WHERE ct.task_id = t.id LIMIT 1);
ALTER TABLE tasks ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE evidences ADD COLUMN tenant_id TEXT;
UPDATE evidences e SET tenant_id = (SELECT t.tenant_id FROM tasks t WHERE t.id = e.task_id);
ALTER TABLE evidences ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE controls_tasks ADD COLUMN tenant_id TEXT;
UPDATE controls_tasks ct SET tenant_id = (SELECT f.tenant_id FROM frameworks f JOIN controls c ON c.framework_id = f.id WHERE c.id = ct.control_id);
ALTER TABLE controls_tasks ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE control_state_transitions ADD COLUMN tenant_id TEXT;
UPDATE control_state_transitions cst SET tenant_id = (SELECT c.tenant_id FROM controls c WHERE c.id = cst.control_id);
ALTER TABLE control_state_transitions ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE task_state_transitions ADD COLUMN tenant_id TEXT;
UPDATE task_state_transitions tst SET tenant_id = (SELECT t.tenant_id FROM tasks t WHERE t.id = tst.task_id);
ALTER TABLE task_state_transitions ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE evidence_state_transitions ADD COLUMN tenant_id TEXT;
UPDATE evidence_state_transitions est SET tenant_id = (SELECT e.tenant_id FROM evidences e WHERE e.id = est.evidence_id);
ALTER TABLE evidence_state_transitions ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE peoples ADD COLUMN tenant_id TEXT;
UPDATE peoples p SET tenant_id = p.organization_id;
ALTER TABLE peoples ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE vendors ADD COLUMN tenant_id TEXT;
UPDATE vendors v SET tenant_id = v.organization_id;
ALTER TABLE vendors ALTER COLUMN tenant_id SET NOT NULL;
ALTER TABLE policies ADD COLUMN tenant_id TEXT;
UPDATE policies p SET tenant_id = p.organization_id;
ALTER TABLE policies ALTER COLUMN tenant_id SET NOT NULL;

View File

@@ -24,10 +24,10 @@ import (
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/api"
console_v1 "github.com/getprobo/probo/pkg/api/console/v1"
"github.com/getprobo/probo/pkg/awsconfig"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/server"
console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1"
"github.com/getprobo/probo/pkg/usrmgr"
"github.com/prometheus/client_golang/prometheus"
"go.gearno.de/kit/httpclient"
@@ -157,11 +157,11 @@ func (impl *Implm) Run(
return fmt.Errorf("cannot create probo service: %w", err)
}
apiServer, err := api.NewServer(
api.Config{
serverHandler, err := server.NewServer(
server.Config{
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
Probo: proboService,
Usrmgr: usrmgrService,
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
Auth: console_v1.AuthConfig{
CookieName: impl.cfg.Auth.CookieName,
CookieSecure: impl.cfg.Auth.CookieSecure,
@@ -174,7 +174,7 @@ func (impl *Implm) Run(
},
)
if err != nil {
return fmt.Errorf("cannot create api server: %w", err)
return fmt.Errorf("cannot create server: %w", err)
}
apiServerCtx, stopApiServer := context.WithCancel(context.Background())
@@ -182,7 +182,7 @@ func (impl *Implm) Run(
wg.Add(1)
go func() {
defer wg.Done()
if err := impl.runApiServer(apiServerCtx, l, r, tp, apiServer); err != nil {
if err := impl.runApiServer(apiServerCtx, l, r, tp, serverHandler); err != nil {
cancel(fmt.Errorf("api server crashed: %w", err))
}
}()

View File

@@ -18,8 +18,8 @@ import (
"errors"
"net/http"
console_v1 "github.com/getprobo/probo/pkg/api/console/v1"
"github.com/getprobo/probo/pkg/probo"
console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1"
"github.com/getprobo/probo/pkg/usrmgr"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"

View File

@@ -21,10 +21,10 @@ call_argument_directives_with_null: true
models:
ID:
model:
- "github.com/getprobo/probo/pkg/api/console/v1/types.GIDScalar"
- "github.com/getprobo/probo/pkg/server/api/console/v1/types.GIDScalar"
Datetime:
model:
- "github.com/99designs/gqlgen/graphql.Time"
CursorKey:
model:
- "github.com/getprobo/probo/pkg/api/console/v1/types.CursorKeyScalar"
- "github.com/getprobo/probo/pkg/server/api/console/v1/types.CursorKeyScalar"

View File

@@ -27,9 +27,9 @@ import (
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/getprobo/probo/pkg/api/console/v1/schema"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/server/api/console/v1/schema"
"github.com/getprobo/probo/pkg/usrmgr"
"github.com/getprobo/probo/pkg/usrmgr/coredata"
"github.com/go-chi/chi/v5"

View File

@@ -9,12 +9,12 @@ import (
"fmt"
"time"
"github.com/getprobo/probo/pkg/api/console/v1/schema"
"github.com/getprobo/probo/pkg/api/console/v1/types"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/probo/coredata"
"github.com/getprobo/probo/pkg/server/api/console/v1/schema"
"github.com/getprobo/probo/pkg/server/api/console/v1/types"
"github.com/vektah/gqlparser/v2/gqlerror"
)

99
pkg/server/server.go Normal file
View File

@@ -0,0 +1,99 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// Package server provides functionality for serving the SPA frontend.
package server
import (
"net/http"
"strings"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/server/api"
console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1"
"github.com/getprobo/probo/pkg/server/web"
"github.com/getprobo/probo/pkg/usrmgr"
"github.com/go-chi/chi/v5"
)
// Config holds the configuration for the server
type Config struct {
AllowedOrigins []string
Probo *probo.Service
Usrmgr *usrmgr.Service
Auth console_v1.AuthConfig
}
// Server represents the main server that handles both API and frontend requests
type Server struct {
apiServer *api.Server
webServer *web.Server
router *chi.Mux
}
// NewServer creates a new server instance
func NewServer(cfg Config) (*Server, error) {
// Create API server
apiCfg := api.Config{
AllowedOrigins: cfg.AllowedOrigins,
Probo: cfg.Probo,
Usrmgr: cfg.Usrmgr,
Auth: cfg.Auth,
}
apiServer, err := api.NewServer(apiCfg)
if err != nil {
return nil, err
}
// Create web server for SPA
webServer, err := web.NewServer()
if err != nil {
return nil, err
}
// Create main router
router := chi.NewRouter()
server := &Server{
apiServer: apiServer,
webServer: webServer,
router: router,
}
// Set up routes
server.setupRoutes()
return server, nil
}
// setupRoutes configures the routing for the server
func (s *Server) setupRoutes() {
// API routes under /api
s.router.Mount("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Strip the /api prefix from the path
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
if r.URL.Path == "" {
r.URL.Path = "/"
}
s.apiServer.ServeHTTP(w, r)
}))
// All other routes go to the SPA frontend
s.router.Mount("/", s.webServer)
}
// ServeHTTP implements the http.Handler interface
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}

83
pkg/server/web/web.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package web
import (
"io/fs"
"log"
"net/http"
"github.com/getprobo/probo/apps/console"
)
type Server struct {
spaFS http.FileSystem
}
func NewServer() (*Server, error) {
subFS, err := fs.Sub(console.StaticFiles, "dist")
if err != nil {
return nil, err
}
log.Printf("Using embedded SPA")
return &Server{
spaFS: http.FS(subFS),
}, nil
}
func (s *Server) ServeSPA(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == "/" {
path = "/index.html"
}
f, err := s.spaFS.Open(path)
if err == nil {
defer f.Close()
http.FileServer(s.spaFS).ServeHTTP(w, r)
return
}
indexFile, err := s.spaFS.Open("/index.html")
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
defer indexFile.Close()
stat, err := indexFile.Stat()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
content := make([]byte, stat.Size())
_, err = indexFile.Read(content)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(content)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.ServeSPA(w, r)
}