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:
14
GNUmakefile
14
GNUmakefile
@@ -35,26 +35,25 @@ vet:
|
||||
$(GO) vet ./...
|
||||
|
||||
.PHONY: build
|
||||
build: bin/probod @probo/console docker-build
|
||||
build: @probo/console bin/probod docker-build
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build:
|
||||
$(DOCKER_BUILD) --tag $(DOCKER_IMAGE_NAME):$(DOCKER_TAG_NAME) --file Dockerfile .
|
||||
|
||||
.PHONY: bin/probod
|
||||
bin/probod: pkg/api/console/v1/schema/schema.go pkg/api/console/v1/types/types.go pkg/api/console/v1/v1_resolver.go vet
|
||||
bin/probod: pkg/server/api/console/v1/schema/schema.go pkg/server/api/console/v1/types/types.go pkg/server/api/console/v1/v1_resolver.go vet
|
||||
$(GO_BUILD) -o $(PROBOD_BIN) $(PROBOD_SRC)
|
||||
|
||||
.PHONY: @probo/console
|
||||
@probo/console: NODE_ENV=production
|
||||
@probo/console:
|
||||
$(NPM) --workspace $@ run typecheck
|
||||
$(NPM) --workspace $@ run build
|
||||
|
||||
pkg/api/console/v1/schema/schema.go \
|
||||
pkg/api/console/v1/types/types.go \
|
||||
pkg/api/console/v1/v1_resolver.go: pkg/api/console/v1/gqlgen.yaml pkg/api/console/v1/schema.graphql
|
||||
$(GO_GENERATE) ./pkg/api/console/v1
|
||||
pkg/server/api/console/v1/schema/schema.go \
|
||||
pkg/server/api/console/v1/types/types.go \
|
||||
pkg/server/api/console/v1/v1_resolver.go: pkg/server/api/console/v1/gqlgen.yaml pkg/server/api/console/v1/schema.graphql
|
||||
$(GO_GENERATE) ./pkg/server/api/console/v1
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: fmt-markdown fmt-go
|
||||
@@ -92,3 +91,4 @@ stack-ps:
|
||||
.PHONY: psql
|
||||
psql:
|
||||
$(DOCKER_COMPOSE) exec postgres psql -U probod -d probod
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
POSTHOG_HOST=
|
||||
POSTHOG_KEY=
|
||||
FARO_PUSH_URL=
|
||||
|
||||
API_SERVER_HOST=
|
||||
|
||||
|
||||
1
apps/console/.gitignore
vendored
1
apps/console/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
!dist/.keep
|
||||
dist/
|
||||
20
apps/console/console.go
Normal file
20
apps/console/console.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 console
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed dist
|
||||
var StaticFiles embed.FS
|
||||
0
apps/console/dist/.keep
vendored
Normal file
0
apps/console/dist/.keep
vendored
Normal file
@@ -70,7 +70,7 @@
|
||||
"relay": {
|
||||
"src": "src",
|
||||
"language": "typescript",
|
||||
"schema": "../../pkg/api/console/v1/schema.graphql",
|
||||
"schema": "../../pkg/server/api/console/v1/schema.graphql",
|
||||
"noFutureProofEnums": true,
|
||||
"customScalarTypes": {
|
||||
"Datetime": "string",
|
||||
|
||||
@@ -11,7 +11,7 @@ const fetchRelay: FetchFunction = async (
|
||||
request,
|
||||
variables,
|
||||
_,
|
||||
uploadables,
|
||||
uploadables
|
||||
) => {
|
||||
const requestInit: RequestInit = {
|
||||
method: "POST",
|
||||
@@ -27,7 +27,7 @@ const fetchRelay: FetchFunction = async (
|
||||
operationName: request.name,
|
||||
query: request.text,
|
||||
variables: variables,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const uploadableMap: {
|
||||
@@ -59,7 +59,10 @@ const fetchRelay: FetchFunction = async (
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(buildEndpoint("/console/v1/query"), requestInit);
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/console/v1/query"),
|
||||
requestInit
|
||||
);
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
@@ -68,8 +71,8 @@ const fetchRelay: FetchFunction = async (
|
||||
`Error fetching GraphQL query '${
|
||||
request.name
|
||||
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
|
||||
json.errors,
|
||||
)}`,
|
||||
json.errors
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const checkAuth = async (): Promise<boolean> => {
|
||||
try {
|
||||
// Make a request to an endpoint that requires authentication
|
||||
const response = await fetch(buildEndpoint("/console/v1/query"), {
|
||||
const response = await fetch(buildEndpoint("/api/console/v1/query"), {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function LoginPage() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(buildEndpoint("/console/v1/auth/login"), {
|
||||
const response = await fetch(buildEndpoint("/api/console/v1/auth/login"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function RegisterPage() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(buildEndpoint("/console/v1/auth/register"), {
|
||||
const response = await fetch(buildEndpoint("/api/console/v1/auth/register"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export function buildEndpoint(path: string): string {
|
||||
const host = process.env.API_SERVER_HOST!;
|
||||
|
||||
if (!host) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const formattedHost =
|
||||
host.startsWith("http://") || host.startsWith("https://")
|
||||
? host
|
||||
: `https://${host}`;
|
||||
|
||||
const url = new URL(formattedHost);
|
||||
|
||||
if (path) {
|
||||
|
||||
47
pkg/probo/coredata/migrations/20250305T155800Z.sql
Normal file
47
pkg/probo/coredata/migrations/20250305T155800Z.sql
Normal 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;
|
||||
@@ -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))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
99
pkg/server/server.go
Normal 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
83
pkg/server/web/web.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user