Refactor hostname to become baseurl

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-10-30 19:41:15 +01:00
parent 27a9858ec0
commit 896e6e0736
29 changed files with 657 additions and 153 deletions

View File

@@ -74,13 +74,13 @@ func (r AccessResult) ToError(baseURL string) error {
case AuthMethodPassword:
return ErrPasswordAuthRequired{
OrganizationID: r.OrganizationID,
RedirectURL: fmt.Sprintf("%s/authentication/login?method=password", baseURL),
RedirectURL: fmt.Sprintf("%s/auth/login?method=password", baseURL),
}
case AuthMethodSAML, AuthMethodAny:
return ErrSAMLAuthRequired{
ConfigID: r.SAMLConfig.ID,
OrganizationID: r.OrganizationID,
RedirectURL: fmt.Sprintf("%s/auth/saml/login/%s", baseURL, r.SAMLConfig.ID),
RedirectURL: fmt.Sprintf("%s/connect/saml/login/%s", baseURL, r.SAMLConfig.ID),
}
default:
return fmt.Errorf("access denied to organization %s", r.OrganizationID)

View File

@@ -224,11 +224,11 @@ func NewSAMLService(
}
func (s *SAMLService) GetEntityID() string {
return fmt.Sprintf("%s/auth/saml/metadata", s.baseURL)
return fmt.Sprintf("%s/connect/saml/metadata", s.baseURL)
}
func (s *SAMLService) GetAcsURL() string {
return fmt.Sprintf("%s/auth/saml/consume", s.baseURL)
return fmt.Sprintf("%s/connect/saml/consume", s.baseURL)
}
func parseRawSAMLResponse(encodedResponse string) (*saml.Assertion, error) {
@@ -564,7 +564,7 @@ func (s *SAMLService) HandleSAMLAssertion(
}
func (s *SAMLService) GetMetadataURL(organizationID gid.GID) string {
return fmt.Sprintf("%s/auth/saml/metadata/%s", s.baseURL, organizationID)
return fmt.Sprintf("%s/connect/saml/metadata/%s", s.baseURL, organizationID)
}
func (s *SAMLService) GenerateMetadata() ([]byte, error) {

View File

@@ -22,10 +22,10 @@ import (
"fmt"
"net"
"net/mail"
"net/url"
"time"
"github.com/getprobo/probo/packages/emails"
"github.com/getprobo/probo/pkg/baseurl"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/crypto/cipher"
"github.com/getprobo/probo/pkg/crypto/passwdhash"
@@ -40,7 +40,6 @@ type (
pg *pg.Client
encryptionKey cipher.EncryptionKey
hp *passwdhash.Profile
hostname string
baseURL string
tokenSecret string
disableSignup bool
@@ -51,7 +50,6 @@ type (
pg *pg.Client
encryptionKey cipher.EncryptionKey
hp *passwdhash.Profile
hostname string
baseURL string
tokenSecret string
scope coredata.Scoper
@@ -187,7 +185,6 @@ func NewService(
encryptionKey cipher.EncryptionKey,
hp *passwdhash.Profile,
tokenSecret string,
hostname string,
baseURL string,
disableSignup bool,
invitationTokenValidity time.Duration,
@@ -196,7 +193,6 @@ func NewService(
pg: pgClient,
encryptionKey: encryptionKey,
hp: hp,
hostname: hostname,
baseURL: baseURL,
tokenSecret: tokenSecret,
disableSignup: disableSignup,
@@ -209,7 +205,6 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthService {
pg: s.pg,
encryptionKey: s.encryptionKey,
hp: s.hp,
hostname: s.hostname,
baseURL: s.baseURL,
tokenSecret: s.tokenSecret,
scope: coredata.NewScope(tenantID),
@@ -232,13 +227,17 @@ func (s Service) ForgetPassword(
return fmt.Errorf("cannot generate password reset token: %w", err)
}
resetPasswordUrl := url.URL{
Scheme: "https",
Host: s.hostname,
Path: "/auth/reset-password",
RawQuery: url.Values{
"token": []string{passwordResetToken},
}.Encode(),
base, err := baseurl.Parse(s.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
resetPasswordUrl, err := base.
WithPath("/auth/reset-password").
WithQuery("token", passwordResetToken).
String()
if err != nil {
return fmt.Errorf("cannot build reset password URL: %w", err)
}
return s.pg.WithConn(
@@ -255,9 +254,9 @@ func (s Service) ForgetPassword(
}
subject, textBody, htmlBody, err := emails.RenderPasswordReset(
s.hostname,
s.baseURL,
user.FullName,
resetPasswordUrl.String(),
resetPasswordUrl,
)
if err != nil {
return fmt.Errorf("cannot render password reset email: %w", err)
@@ -352,19 +351,23 @@ func (s Service) SignUp(
return fmt.Errorf("cannot generate confirmation token: %w", err)
}
confirmationUrl := url.URL{
Scheme: "https",
Host: s.hostname,
Path: "/auth/confirm-email",
RawQuery: url.Values{
"token": []string{confirmationToken},
}.Encode(),
base, err := baseurl.Parse(s.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
confirmationUrl, err := base.
WithPath("/auth/confirm-email").
WithQuery("token", confirmationToken).
String()
if err != nil {
return fmt.Errorf("cannot build confirmation URL: %w", err)
}
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
s.hostname,
s.baseURL,
user.FullName,
confirmationUrl.String(),
confirmationUrl,
)
if err != nil {
return fmt.Errorf("cannot render confirmation email: %w", err)

View File

@@ -32,14 +32,14 @@ import (
type (
Service struct {
pg *pg.Client
hostname string
baseURL string
tokenSecret string
invitationTokenValidity time.Duration
}
TenantAuthzService struct {
pg *pg.Client
hostname string
baseURL string
tokenSecret string
invitationTokenValidity time.Duration
scope coredata.Scoper
@@ -62,13 +62,13 @@ const (
func NewService(
ctx context.Context,
pgClient *pg.Client,
hostname string,
baseURL string,
tokenSecret string,
invitationTokenValidity time.Duration,
) (*Service, error) {
return &Service{
pg: pgClient,
hostname: hostname,
baseURL: baseURL,
tokenSecret: tokenSecret,
invitationTokenValidity: invitationTokenValidity,
}, nil
@@ -77,7 +77,7 @@ func NewService(
func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthzService {
return &TenantAuthzService{
pg: s.pg,
hostname: s.hostname,
baseURL: s.baseURL,
tokenSecret: s.tokenSecret,
invitationTokenValidity: s.invitationTokenValidity,
scope: coredata.NewScope(tenantID),
@@ -743,7 +743,7 @@ func (s *TenantAuthzService) InviteUserToOrganization(
if userExists {
recipientName = user.FullName
invitationURL = fmt.Sprintf("https://%s/", s.hostname)
invitationURL = s.baseURL + "/"
} else {
recipientName = fullName
invitationData := coredata.InvitationData{
@@ -764,11 +764,11 @@ func (s *TenantAuthzService) InviteUserToOrganization(
return fmt.Errorf("cannot generate invitation token: %w", err)
}
invitationURL = fmt.Sprintf("https://%s/auth/signup-from-invitation?token=%s&fullName=%s", s.hostname, invitationToken, url.QueryEscape(fullName))
invitationURL = fmt.Sprintf("%s/auth/signup-from-invitation?token=%s&fullName=%s", s.baseURL, invitationToken, url.QueryEscape(fullName))
}
subject, textBody, htmlBody, err := emails.RenderInvitation(
s.hostname,
s.baseURL,
recipientName,
organization.Name,
invitationURL,

226
pkg/baseurl/baseurl.go Normal file
View File

@@ -0,0 +1,226 @@
// 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 baseurl
import (
"encoding/json"
"fmt"
"net/url"
"strings"
)
// BaseURL represents a validated base URL for the application.
// It provides convenient methods for building URLs with paths and query parameters.
type BaseURL struct {
raw string
parsed *url.URL
}
// Parse creates a new BaseURL from a string, validating that it's a valid absolute URL.
func Parse(rawURL string) (*BaseURL, error) {
if rawURL == "" {
return nil, fmt.Errorf("base URL cannot be empty")
}
parsed, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}
if !parsed.IsAbs() {
return nil, fmt.Errorf("base URL must be absolute (include scheme)")
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("base URL scheme must be http or https, got: %s", parsed.Scheme)
}
if parsed.Host == "" {
return nil, fmt.Errorf("base URL must include a host")
}
return &BaseURL{
raw: rawURL,
parsed: parsed,
}, nil
}
// MustParse creates a new BaseURL from a string, panicking if it's invalid.
// This should only be used in tests or with known-valid URLs.
func MustParse(rawURL string) *BaseURL {
b, err := Parse(rawURL)
if err != nil {
panic(err)
}
return b
}
// String returns the base URL as a string.
func (b *BaseURL) String() string {
if b == nil {
return ""
}
return b.raw
}
// Scheme returns the URL scheme (http or https).
func (b *BaseURL) Scheme() string {
if b == nil || b.parsed == nil {
return ""
}
return b.parsed.Scheme
}
// Host returns the host:port portion of the URL.
func (b *BaseURL) Host() string {
if b == nil || b.parsed == nil {
return ""
}
return b.parsed.Host
}
// Hostname returns just the hostname without the port.
func (b *BaseURL) Hostname() string {
if b == nil || b.parsed == nil {
return ""
}
return b.parsed.Hostname()
}
// Port returns the port portion of the URL, or empty string if not specified.
func (b *BaseURL) Port() string {
if b == nil || b.parsed == nil {
return ""
}
return b.parsed.Port()
}
// URLBuilder provides a fluent interface for building URLs.
type URLBuilder struct {
base *BaseURL
path string
query url.Values
err error
}
// WithPath returns a URLBuilder with the specified path.
// The path will be properly joined with the base URL.
func (b *BaseURL) WithPath(path string) *URLBuilder {
if b == nil {
return &URLBuilder{err: fmt.Errorf("base URL is nil")}
}
// Ensure path starts with /
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return &URLBuilder{
base: b,
path: path,
query: make(url.Values),
}
}
// WithQuery adds a query parameter to the URL.
func (ub *URLBuilder) WithQuery(key, value string) *URLBuilder {
if ub.err != nil {
return ub
}
ub.query.Add(key, value)
return ub
}
// WithQueryValues sets multiple query parameters at once.
func (ub *URLBuilder) WithQueryValues(values url.Values) *URLBuilder {
if ub.err != nil {
return ub
}
for key, vals := range values {
for _, val := range vals {
ub.query.Add(key, val)
}
}
return ub
}
// String builds and returns the final URL string.
func (ub *URLBuilder) String() (string, error) {
if ub.err != nil {
return "", ub.err
}
u := &url.URL{
Scheme: ub.base.Scheme(),
Host: ub.base.Host(),
Path: ub.path,
RawQuery: ub.query.Encode(),
}
return u.String(), nil
}
// MustString builds and returns the final URL string, panicking on error.
// This should only be used when you're certain the URL is valid.
func (ub *URLBuilder) MustString() string {
s, err := ub.String()
if err != nil {
panic(err)
}
return s
}
// UnmarshalJSON implements json.Unmarshaler for BaseURL.
func (b *BaseURL) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsed, err := Parse(s)
if err != nil {
return err
}
*b = *parsed
return nil
}
// MarshalJSON implements json.Marshaler for BaseURL.
func (b *BaseURL) MarshalJSON() ([]byte, error) {
if b == nil {
return json.Marshal("")
}
return json.Marshal(b.raw)
}
// UnmarshalText implements encoding.TextUnmarshaler for BaseURL.
func (b *BaseURL) UnmarshalText(text []byte) error {
parsed, err := Parse(string(text))
if err != nil {
return err
}
*b = *parsed
return nil
}
// MarshalText implements encoding.TextMarshaler for BaseURL.
func (b *BaseURL) MarshalText() ([]byte, error) {
if b == nil {
return []byte(""), nil
}
return []byte(b.raw), nil
}

243
pkg/baseurl/baseurl_test.go Normal file
View File

@@ -0,0 +1,243 @@
// 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 baseurl
import (
"encoding/json"
"net/url"
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "valid http URL",
input: "http://localhost:8080",
wantErr: false,
},
{
name: "valid https URL",
input: "https://example.com",
wantErr: false,
},
{
name: "valid https URL with port",
input: "https://example.com:8443",
wantErr: false,
},
{
name: "empty string",
input: "",
wantErr: true,
},
{
name: "relative URL",
input: "/path/to/resource",
wantErr: true,
},
{
name: "invalid scheme",
input: "ftp://example.com",
wantErr: true,
},
{
name: "no host",
input: "http://",
wantErr: true,
},
{
name: "invalid URL",
input: "ht!tp://invalid",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got == nil {
t.Error("Parse() returned nil without error")
}
})
}
}
func TestBaseURL_Accessors(t *testing.T) {
b := MustParse("https://example.com:8443")
if got := b.String(); got != "https://example.com:8443" {
t.Errorf("String() = %v, want %v", got, "https://example.com:8443")
}
if got := b.Scheme(); got != "https" {
t.Errorf("Scheme() = %v, want %v", got, "https")
}
if got := b.Host(); got != "example.com:8443" {
t.Errorf("Host() = %v, want %v", got, "example.com:8443")
}
if got := b.Hostname(); got != "example.com" {
t.Errorf("Hostname() = %v, want %v", got, "example.com")
}
if got := b.Port(); got != "8443" {
t.Errorf("Port() = %v, want %v", got, "8443")
}
}
func TestBaseURL_WithPath(t *testing.T) {
b := MustParse("https://example.com")
tests := []struct {
name string
path string
want string
}{
{
name: "path with leading slash",
path: "/auth/login",
want: "https://example.com/auth/login",
},
{
name: "path without leading slash",
path: "auth/login",
want: "https://example.com/auth/login",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := b.WithPath(tt.path).String()
if err != nil {
t.Errorf("WithPath().String() error = %v", err)
return
}
if got != tt.want {
t.Errorf("WithPath().String() = %v, want %v", got, tt.want)
}
})
}
}
func TestBaseURL_WithQuery(t *testing.T) {
b := MustParse("https://example.com")
got, err := b.WithPath("/search").
WithQuery("q", "test").
WithQuery("limit", "10").
String()
if err != nil {
t.Fatalf("WithPath().WithQuery().String() error = %v", err)
}
// Parse the result to check query parameters
parsed, err := url.Parse(got)
if err != nil {
t.Fatalf("Failed to parse result URL: %v", err)
}
if parsed.Query().Get("q") != "test" {
t.Errorf("Query param 'q' = %v, want %v", parsed.Query().Get("q"), "test")
}
if parsed.Query().Get("limit") != "10" {
t.Errorf("Query param 'limit' = %v, want %v", parsed.Query().Get("limit"), "10")
}
}
func TestBaseURL_WithQueryValues(t *testing.T) {
b := MustParse("https://example.com")
values := url.Values{}
values.Add("foo", "bar")
values.Add("baz", "qux")
got, err := b.WithPath("/test").WithQueryValues(values).String()
if err != nil {
t.Fatalf("WithPath().WithQueryValues().String() error = %v", err)
}
parsed, err := url.Parse(got)
if err != nil {
t.Fatalf("Failed to parse result URL: %v", err)
}
if parsed.Query().Get("foo") != "bar" {
t.Errorf("Query param 'foo' = %v, want %v", parsed.Query().Get("foo"), "bar")
}
if parsed.Query().Get("baz") != "qux" {
t.Errorf("Query param 'baz' = %v, want %v", parsed.Query().Get("baz"), "qux")
}
}
func TestBaseURL_JSON(t *testing.T) {
original := MustParse("https://example.com:8443")
// Marshal
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
// Unmarshal
var restored BaseURL
if err := json.Unmarshal(data, &restored); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if restored.String() != original.String() {
t.Errorf("After JSON round-trip: got %v, want %v", restored.String(), original.String())
}
}
func TestBaseURL_NilSafety(t *testing.T) {
var b *BaseURL
if got := b.String(); got != "" {
t.Errorf("nil.String() = %v, want empty string", got)
}
if got := b.Scheme(); got != "" {
t.Errorf("nil.Scheme() = %v, want empty string", got)
}
if got := b.Host(); got != "" {
t.Errorf("nil.Host() = %v, want empty string", got)
}
if got := b.Hostname(); got != "" {
t.Errorf("nil.Hostname() = %v, want empty string", got)
}
if got := b.Port(); got != "" {
t.Errorf("nil.Port() = %v, want empty string", got)
}
builder := b.WithPath("/test")
if _, err := builder.String(); err == nil {
t.Error("nil.WithPath().String() expected error, got nil")
}
}

View File

@@ -457,9 +457,14 @@ func (s *DocumentService) SendSigningNotifications(
return fmt.Errorf("cannot create signing request token: %w", err)
}
baseURLParsed, err := url.Parse(s.svc.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
signRequestURL := url.URL{
Scheme: "https",
Host: s.svc.hostname,
Scheme: baseURLParsed.Scheme,
Host: baseURLParsed.Host,
Path: "/documents/signing-requests",
RawQuery: url.Values{
"token": []string{token},
@@ -467,7 +472,7 @@ func (s *DocumentService) SendSigningNotifications(
}
subject, textBody, htmlBody, err := emails.RenderDocumentSigning(
s.svc.hostname,
s.svc.baseURL,
people.FullName,
organization.Name,
signRequestURL.String(),
@@ -1584,7 +1589,7 @@ func (s *DocumentService) SendExportEmail(
}
subject, textBody, htmlBody, err := emails.RenderDocumentExport(
s.svc.hostname,
s.svc.baseURL,
recipientName,
downloadURL,
)

View File

@@ -709,7 +709,7 @@ func (s FrameworkService) SendExportEmail(
}
subject, textBody, htmlBody, err := emails.RenderFrameworkExport(
s.svc.hostname,
s.svc.baseURL,
recipientName,
downloadURL,
)

View File

@@ -52,7 +52,7 @@ type (
s3 *s3.Client
bucket string
encryptionKey cipher.EncryptionKey
hostname string
baseURL string
tokenSecret string
trustConfig TrustConfig
agentConfig agents.Config
@@ -70,7 +70,7 @@ type (
bucket string
encryptionKey cipher.EncryptionKey
scope coredata.Scoper
hostname string
baseURL string
tokenSecret string
trustConfig TrustConfig
agent *agents.Agent
@@ -115,7 +115,7 @@ func NewService(
pgClient *pg.Client,
s3Client *s3.Client,
bucket string,
hostname string,
baseURL string,
tokenSecret string,
trustConfig TrustConfig,
agentConfig agents.Config,
@@ -135,7 +135,7 @@ func NewService(
s3: s3Client,
bucket: bucket,
encryptionKey: encryptionKey,
hostname: hostname,
baseURL: baseURL,
tokenSecret: tokenSecret,
trustConfig: trustConfig,
agentConfig: agentConfig,
@@ -156,7 +156,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
s3: s.s3,
bucket: s.bucket,
encryptionKey: s.encryptionKey,
hostname: s.hostname,
baseURL: s.baseURL,
scope: coredata.NewScope(tenantID),
tokenSecret: s.tokenSecret,
trustConfig: s.trustConfig,

View File

@@ -442,7 +442,13 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
return fmt.Errorf("cannot load organization: %w", err)
}
hostname := s.svc.hostname
baseURLParsed, err := url.Parse(s.svc.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
hostname := baseURLParsed.Host
scheme := baseURLParsed.Scheme
path := "/trust/" + trustCenter.Slug + "/access"
if organization.CustomDomainID != nil {
@@ -456,11 +462,12 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
}
hostname = customDomain.Domain
scheme = "https"
path = "/access"
}
accessURL := url.URL{
Scheme: "https",
Scheme: scheme,
Host: hostname,
Path: path,
RawQuery: url.Values{
@@ -489,7 +496,7 @@ func (s TrustCenterAccessService) sendTrustCenterAccessEmail(
accessURL string,
) error {
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
s.svc.hostname,
s.svc.baseURL,
name,
companyName,
accessURL,

View File

@@ -31,6 +31,7 @@ import (
"github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/authz"
"github.com/getprobo/probo/pkg/awsconfig"
"github.com/getprobo/probo/pkg/baseurl"
"github.com/getprobo/probo/pkg/certmanager"
"github.com/getprobo/probo/pkg/connector"
"github.com/getprobo/probo/pkg/coredata"
@@ -64,7 +65,7 @@ type (
}
config struct {
Hostname string `json:"hostname"`
BaseURL *baseurl.BaseURL `json:"base-url"`
EncryptionKey cipher.EncryptionKey `json:"encryption-key"`
Pg pgConfig `json:"pg"`
Api apiConfig `json:"api"`
@@ -93,7 +94,7 @@ var (
func New() *Implm {
return &Implm{
cfg: config{
Hostname: "localhost:8080",
BaseURL: baseurl.MustParse("http://localhost:8080"),
Api: apiConfig{
Addr: "localhost:8080",
},
@@ -275,8 +276,7 @@ func (impl *Implm) Run(
impl.cfg.EncryptionKey,
hp,
impl.cfg.Auth.Cookie.Secret,
impl.cfg.Hostname,
fmt.Sprintf("https://%s", impl.cfg.Hostname),
impl.cfg.BaseURL.String(),
impl.cfg.Auth.DisableSignup,
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
)
@@ -287,7 +287,7 @@ func (impl *Implm) Run(
authzService, err := authz.NewService(
ctx,
pgClient,
impl.cfg.Hostname,
impl.cfg.BaseURL.String(),
impl.cfg.Auth.Cookie.Secret,
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
)
@@ -300,7 +300,7 @@ func (impl *Implm) Run(
samlService, err := auth.NewSAMLService(
pgClient,
impl.cfg.EncryptionKey,
fmt.Sprintf("https://%s", impl.cfg.Hostname),
impl.cfg.BaseURL.String(),
impl.cfg.Auth.SAML.SessionDurationTime(),
impl.cfg.Auth.Cookie.Name,
impl.cfg.Auth.Cookie.Secret,
@@ -347,7 +347,7 @@ func (impl *Implm) Run(
pgClient,
s3Client,
impl.cfg.AWS.Bucket,
impl.cfg.Hostname,
impl.cfg.BaseURL.String(),
impl.cfg.Auth.Cookie.Secret,
trustConfig,
agentConfig,
@@ -366,7 +366,7 @@ func (impl *Implm) Run(
pgClient,
s3Client,
impl.cfg.AWS.Bucket,
impl.cfg.Hostname,
impl.cfg.BaseURL.String(),
impl.cfg.EncryptionKey,
impl.cfg.TrustAuth.TokenSecret,
impl.cfg.GetSlackSigningSecret(),
@@ -392,7 +392,7 @@ func (impl *Implm) Run(
SAML: samlService,
ConnectorRegistry: defaultConnectorRegistry,
Agent: agent,
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname},
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.BaseURL.Host()},
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
FileManager: fileManagerService,
PGClient: pgClient,

View File

@@ -4997,12 +4997,12 @@ func (r *sAMLConfigurationResolver) SpMetadataURL(ctx context.Context, obj *type
// TestLoginURL is the resolver for the testLoginUrl field.
func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
entityID := r.samlSvc.GetEntityID()
parts := strings.Split(entityID, "/auth/saml/metadata")
parts := strings.Split(entityID, "/connect/saml/metadata")
if len(parts) != 2 {
return "", fmt.Errorf("invalid entity ID format")
}
return fmt.Sprintf("%s/auth/saml/login/%s", parts[0], obj.ID), nil
return fmt.Sprintf("%s/connect/saml/login/%s", parts[0], obj.ID), nil
}
// Organization is the resolver for the organization field.

View File

@@ -56,7 +56,7 @@ func buildOrganizationResponse(
// Generate logo URL path if organization has a logo
var logoURL *string
if org.LogoFileID != nil {
url := fmt.Sprintf("/auth/organizations/%s/logo", org.ID)
url := fmt.Sprintf("/connect/organizations/%s/logo", org.ID)
logoURL = &url
}
@@ -74,11 +74,11 @@ func buildOrganizationResponse(
case authsvc.AuthMethodSAML, authsvc.AuthMethodAny:
orgResponse.AuthenticationMethod = "saml"
if accessResult.SAMLConfig != nil {
orgResponse.LoginURL = fmt.Sprintf("/auth/saml/login/%s", accessResult.SAMLConfig.ID)
orgResponse.LoginURL = fmt.Sprintf("/connect/saml/login/%s", accessResult.SAMLConfig.ID)
}
case authsvc.AuthMethodPassword:
orgResponse.AuthenticationMethod = "password"
orgResponse.LoginURL = "/authentication/login?method=password"
orgResponse.LoginURL = "/auth/login?method=password"
}
return orgResponse
}
@@ -88,13 +88,13 @@ func buildOrganizationResponse(
if sessionData.PasswordAuthenticated {
orgResponse.AuthenticationMethod = "password"
orgResponse.LoginURL = "/authentication/login?method=password"
orgResponse.LoginURL = "/auth/login?method=password"
} else if samlInfo, ok := sessionData.SAMLAuthenticatedOrgs[org.ID.String()]; ok {
orgResponse.AuthenticationMethod = "saml"
orgResponse.LoginURL = fmt.Sprintf("/auth/saml/login/%s", samlInfo.SAMLConfigID)
orgResponse.LoginURL = fmt.Sprintf("/connect/saml/login/%s", samlInfo.SAMLConfigID)
} else {
orgResponse.AuthenticationMethod = "any"
orgResponse.LoginURL = "/authentication/login?method=password"
orgResponse.LoginURL = "/auth/login?method=password"
}
return orgResponse

View File

@@ -135,7 +135,7 @@ func NewServer(cfg Config) (*Server, error) {
func (s *Server) setupRoutes() {
s.router.Mount("/api", s.apiServer)
s.router.Mount("/auth", s.authServer)
s.router.Mount("/connect", s.authServer)
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
r.Use(s.loadTrustCenterBySlugOrID)

View File

@@ -45,7 +45,7 @@ type (
encryptionKey cipher.EncryptionKey
tokenSecret string
slackSigningSecret string
hostname string
baseURL string
auth *auth.Service
html2pdfConverter *html2pdf.Converter
fileManager *filemanager.Service
@@ -61,7 +61,7 @@ type (
proboSvc *probo.Service
encryptionKey cipher.EncryptionKey
tokenSecret string
hostname string
baseURL string
auth *auth.Service
html2pdfConverter *html2pdf.Converter
fileManager *filemanager.Service
@@ -85,7 +85,7 @@ func NewService(
pgClient *pg.Client,
s3Client *s3.Client,
bucket string,
hostname string,
baseURL string,
encryptionKey cipher.EncryptionKey,
tokenSecret string,
slackSigningSecret string,
@@ -102,7 +102,7 @@ func NewService(
encryptionKey: encryptionKey,
tokenSecret: tokenSecret,
slackSigningSecret: slackSigningSecret,
hostname: hostname,
baseURL: baseURL,
auth: auth,
html2pdfConverter: html2pdfConverter,
fileManager: fileManagerService,
@@ -120,7 +120,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
proboSvc: s.proboSvc,
encryptionKey: s.encryptionKey,
tokenSecret: s.tokenSecret,
hostname: s.hostname,
baseURL: s.baseURL,
auth: s.auth,
html2pdfConverter: s.html2pdfConverter,
fileManager: s.fileManager,

View File

@@ -22,6 +22,7 @@ import (
"fmt"
"time"
"github.com/getprobo/probo/pkg/baseurl"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/slack"
@@ -409,6 +410,11 @@ func (s *SlackMessageService) buildAccessRequestMessage(
fileIDs = append(fileIDs, file.ID)
}
base, err := baseurl.Parse(s.svc.baseURL)
if err != nil {
return nil, fmt.Errorf("cannot parse base URL: %w", err)
}
templateData := struct {
RequesterName string
RequesterEmail string
@@ -425,7 +431,7 @@ func (s *SlackMessageService) buildAccessRequestMessage(
RequesterName: requesterName,
RequesterEmail: requesterEmail,
OrganizationID: organizationID.String(),
Domain: s.svc.hostname,
Domain: base.Host(),
SlackMessageID: slackMessageID.String(),
DocumentIDs: documentIDs,
ReportIDs: reportIDs,

View File

@@ -470,7 +470,13 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
return fmt.Errorf("cannot load organization: %w", err)
}
hostname := s.svc.hostname
baseURLParsed, err := url.Parse(s.svc.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
hostname := baseURLParsed.Host
scheme := baseURLParsed.Scheme
path := "/trust/" + trustCenter.Slug + "/access"
if organization.CustomDomainID != nil {
@@ -484,11 +490,12 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
}
hostname = customDomain.Domain
scheme = "https"
path = "/access"
}
accessURL := url.URL{
Scheme: "https",
Scheme: scheme,
Host: hostname,
Path: path,
RawQuery: url.Values{
@@ -517,7 +524,7 @@ func (s *TrustCenterAccessService) sendTrustCenterAccessEmail(
accessURL string,
) error {
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
s.svc.hostname,
s.svc.baseURL,
name,
companyName,
accessURL,