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 ./...
|
$(GO) vet ./...
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: bin/probod @probo/console docker-build
|
build: @probo/console bin/probod docker-build
|
||||||
|
|
||||||
.PHONY: docker-build
|
.PHONY: docker-build
|
||||||
docker-build:
|
docker-build:
|
||||||
$(DOCKER_BUILD) --tag $(DOCKER_IMAGE_NAME):$(DOCKER_TAG_NAME) --file Dockerfile .
|
$(DOCKER_BUILD) --tag $(DOCKER_IMAGE_NAME):$(DOCKER_TAG_NAME) --file Dockerfile .
|
||||||
|
|
||||||
.PHONY: bin/probod
|
.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)
|
$(GO_BUILD) -o $(PROBOD_BIN) $(PROBOD_SRC)
|
||||||
|
|
||||||
.PHONY: @probo/console
|
.PHONY: @probo/console
|
||||||
@probo/console: NODE_ENV=production
|
@probo/console: NODE_ENV=production
|
||||||
@probo/console:
|
@probo/console:
|
||||||
$(NPM) --workspace $@ run typecheck
|
|
||||||
$(NPM) --workspace $@ run build
|
$(NPM) --workspace $@ run build
|
||||||
|
|
||||||
pkg/api/console/v1/schema/schema.go \
|
pkg/server/api/console/v1/schema/schema.go \
|
||||||
pkg/api/console/v1/types/types.go \
|
pkg/server/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
|
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/api/console/v1
|
$(GO_GENERATE) ./pkg/server/api/console/v1
|
||||||
|
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: fmt-markdown fmt-go
|
fmt: fmt-markdown fmt-go
|
||||||
@@ -92,3 +91,4 @@ stack-ps:
|
|||||||
.PHONY: psql
|
.PHONY: psql
|
||||||
psql:
|
psql:
|
||||||
$(DOCKER_COMPOSE) exec postgres psql -U probod -d probod
|
$(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/
|
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": {
|
"relay": {
|
||||||
"src": "src",
|
"src": "src",
|
||||||
"language": "typescript",
|
"language": "typescript",
|
||||||
"schema": "../../pkg/api/console/v1/schema.graphql",
|
"schema": "../../pkg/server/api/console/v1/schema.graphql",
|
||||||
"noFutureProofEnums": true,
|
"noFutureProofEnums": true,
|
||||||
"customScalarTypes": {
|
"customScalarTypes": {
|
||||||
"Datetime": "string",
|
"Datetime": "string",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const fetchRelay: FetchFunction = async (
|
|||||||
request,
|
request,
|
||||||
variables,
|
variables,
|
||||||
_,
|
_,
|
||||||
uploadables,
|
uploadables
|
||||||
) => {
|
) => {
|
||||||
const requestInit: RequestInit = {
|
const requestInit: RequestInit = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -27,7 +27,7 @@ const fetchRelay: FetchFunction = async (
|
|||||||
operationName: request.name,
|
operationName: request.name,
|
||||||
query: request.text,
|
query: request.text,
|
||||||
variables: variables,
|
variables: variables,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const uploadableMap: {
|
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();
|
const json = await response.json();
|
||||||
|
|
||||||
@@ -68,8 +71,8 @@ const fetchRelay: FetchFunction = async (
|
|||||||
`Error fetching GraphQL query '${
|
`Error fetching GraphQL query '${
|
||||||
request.name
|
request.name
|
||||||
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
|
}' 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> => {
|
const checkAuth = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
// Make a request to an endpoint that requires authentication
|
// 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",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function LoginPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildEndpoint("/console/v1/auth/login"), {
|
const response = await fetch(buildEndpoint("/api/console/v1/auth/login"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function RegisterPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildEndpoint("/console/v1/auth/register"), {
|
const response = await fetch(buildEndpoint("/api/console/v1/auth/register"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
export function buildEndpoint(path: string): string {
|
export function buildEndpoint(path: string): string {
|
||||||
const host = process.env.API_SERVER_HOST!;
|
const host = process.env.API_SERVER_HOST!;
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
const formattedHost =
|
const formattedHost =
|
||||||
host.startsWith("http://") || host.startsWith("https://")
|
host.startsWith("http://") || host.startsWith("https://")
|
||||||
? host
|
? host
|
||||||
: `https://${host}`;
|
: `https://${host}`;
|
||||||
|
|
||||||
const url = new URL(formattedHost);
|
const url = new URL(formattedHost);
|
||||||
|
|
||||||
if (path) {
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"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/awsconfig"
|
||||||
"github.com/getprobo/probo/pkg/probo"
|
"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/getprobo/probo/pkg/usrmgr"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.gearno.de/kit/httpclient"
|
"go.gearno.de/kit/httpclient"
|
||||||
@@ -157,11 +157,11 @@ func (impl *Implm) Run(
|
|||||||
return fmt.Errorf("cannot create probo service: %w", err)
|
return fmt.Errorf("cannot create probo service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiServer, err := api.NewServer(
|
serverHandler, err := server.NewServer(
|
||||||
api.Config{
|
server.Config{
|
||||||
|
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
||||||
Probo: proboService,
|
Probo: proboService,
|
||||||
Usrmgr: usrmgrService,
|
Usrmgr: usrmgrService,
|
||||||
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
|
||||||
Auth: console_v1.AuthConfig{
|
Auth: console_v1.AuthConfig{
|
||||||
CookieName: impl.cfg.Auth.CookieName,
|
CookieName: impl.cfg.Auth.CookieName,
|
||||||
CookieSecure: impl.cfg.Auth.CookieSecure,
|
CookieSecure: impl.cfg.Auth.CookieSecure,
|
||||||
@@ -174,7 +174,7 @@ func (impl *Implm) Run(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
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())
|
apiServerCtx, stopApiServer := context.WithCancel(context.Background())
|
||||||
@@ -182,7 +182,7 @@ func (impl *Implm) Run(
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
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))
|
cancel(fmt.Errorf("api server crashed: %w", err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
console_v1 "github.com/getprobo/probo/pkg/api/console/v1"
|
|
||||||
"github.com/getprobo/probo/pkg/probo"
|
"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/getprobo/probo/pkg/usrmgr"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
@@ -21,10 +21,10 @@ call_argument_directives_with_null: true
|
|||||||
models:
|
models:
|
||||||
ID:
|
ID:
|
||||||
model:
|
model:
|
||||||
- "github.com/getprobo/probo/pkg/api/console/v1/types.GIDScalar"
|
- "github.com/getprobo/probo/pkg/server/api/console/v1/types.GIDScalar"
|
||||||
Datetime:
|
Datetime:
|
||||||
model:
|
model:
|
||||||
- "github.com/99designs/gqlgen/graphql.Time"
|
- "github.com/99designs/gqlgen/graphql.Time"
|
||||||
CursorKey:
|
CursorKey:
|
||||||
model:
|
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/extension"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||||
"github.com/99designs/gqlgen/graphql/playground"
|
"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/gid"
|
||||||
"github.com/getprobo/probo/pkg/probo"
|
"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"
|
||||||
"github.com/getprobo/probo/pkg/usrmgr/coredata"
|
"github.com/getprobo/probo/pkg/usrmgr/coredata"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"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/gid"
|
||||||
"github.com/getprobo/probo/pkg/page"
|
"github.com/getprobo/probo/pkg/page"
|
||||||
"github.com/getprobo/probo/pkg/probo"
|
"github.com/getprobo/probo/pkg/probo"
|
||||||
"github.com/getprobo/probo/pkg/probo/coredata"
|
"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"
|
"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