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

@@ -154,7 +154,7 @@ export function MainLayout() {
icon={IconBook}
to={`${prefix}/obligations`}
/>
<SidebarItem
<SidebarItem
label={__("Continual Improvements")}
icon={IconRotateCw}
to={`${prefix}/continual-improvements`}
@@ -192,15 +192,16 @@ export function MainLayout() {
function UserDropdown({ organizationId }: { organizationId: string }) {
const { __ } = useTranslate();
const { toast } = useToast();
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId }).viewer
.user;
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
organizationId,
}).viewer.user;
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (
e
) => {
e.preventDefault();
fetch(buildEndpoint("/auth/logout"), {
fetch(buildEndpoint("/connect/logout"), {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@@ -274,13 +275,19 @@ interface InvitationsResponse {
invitations: Invitation[];
}
function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) {
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId });
function OrganizationSelectorWrapper({
organizationId,
}: {
organizationId: string;
}) {
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
organizationId,
});
return <OrganizationSelector currentOrganization={data.organization} />;
}
function OrganizationSelector({
currentOrganization
currentOrganization,
}: {
currentOrganization: MainLayoutQueryType["response"]["organization"];
}) {
@@ -297,32 +304,32 @@ function OrganizationSelector({
// Fetch organizations and invitations in parallel
const [orgsResponse, invitationsResponse] = await Promise.all([
fetch('/auth/organizations', { credentials: 'include' }),
fetch('/auth/invitations', { credentials: 'include' })
fetch("/connect/organizations", { credentials: "include" }),
fetch("/connect/invitations", { credentials: "include" }),
]);
if (!orgsResponse.ok) {
throw new Error('Failed to fetch organizations');
throw new Error("Failed to fetch organizations");
}
if (!invitationsResponse.ok) {
throw new Error('Failed to fetch invitations');
throw new Error("Failed to fetch invitations");
}
const orgsData: OrganizationsResponse = await orgsResponse.json();
const invitationsData: InvitationsResponse = await invitationsResponse.json();
const invitationsData: InvitationsResponse =
await invitationsResponse.json();
// Count pending invitations (those without acceptedAt)
const pendingCount = invitationsData.invitations.filter(
inv => !inv.acceptedAt
(inv) => !inv.acceptedAt
).length;
setOrganizations(orgsData.organizations);
setPendingInvitationsCount(pendingCount);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
console.error('Failed to fetch data:', err);
setError(err instanceof Error ? err.message : "Unknown error");
console.error("Failed to fetch data:", err);
} finally {
setIsLoading(false);
}
@@ -334,11 +341,7 @@ function OrganizationSelector({
if (error) {
return (
<div className="flex items-center gap-1">
<Button
className="-ml-3"
variant="tertiary"
disabled
>
<Button className="-ml-3" variant="tertiary" disabled>
{__("Error loading organizations")}
</Button>
</div>
@@ -355,7 +358,7 @@ function OrganizationSelector({
iconAfter={IconChevronGrabberVertical}
disabled={isLoading}
>
{isLoading ? __("Loading...") : (currentOrganization?.name || "")}
{isLoading ? __("Loading...") : currentOrganization?.name || ""}
</Button>
}
>
@@ -370,7 +373,8 @@ function OrganizationSelector({
</div>
) : (
organizations.map((organization) => {
const isAuthenticated = organization.authStatus === "authenticated";
const isAuthenticated =
organization.authStatus === "authenticated";
const isExpired = organization.authStatus === "expired";
const needsAuth = organization.authStatus === "unauthenticated";
@@ -378,16 +382,16 @@ function OrganizationSelector({
? `/organizations/${organization.id}`
: organization.loginUrl;
const isSAMLUrl = targetUrl.includes('/auth/saml/');
const isSAMLUrl = targetUrl.includes("/connect/saml/");
return (
<DropdownItem
asChild
key={organization.id}
>
<DropdownItem asChild key={organization.id}>
{isSAMLUrl ? (
<a href={targetUrl} className="flex items-center gap-2">
<Avatar name={organization.name} src={organization.logoUrl} />
<Avatar
name={organization.name}
src={organization.logoUrl}
/>
<span className="flex-1">{organization.name}</span>
{isAuthenticated && (
<IconCheckmark1 size={16} className="text-green-600" />
@@ -401,7 +405,10 @@ function OrganizationSelector({
</a>
) : (
<Link to={targetUrl} className="flex items-center gap-2">
<Avatar name={organization.name} src={organization.logoUrl} />
<Avatar
name={organization.name}
src={organization.logoUrl}
/>
<span className="flex-1">{organization.name}</span>
{isAuthenticated && (
<IconCheckmark1 size={16} className="text-green-600" />

View File

@@ -51,7 +51,7 @@ export default function OrganizationsPage() {
useEffect(() => {
const fetchOrganizations = async () => {
try {
const response = await fetch('/auth/organizations', {
const response = await fetch('/connect/organizations', {
credentials: 'include',
});
@@ -75,7 +75,7 @@ export default function OrganizationsPage() {
useEffect(() => {
const fetchInvitations = async () => {
try {
const response = await fetch('/auth/invitations', {
const response = await fetch('/connect/invitations', {
credentials: 'include',
});
@@ -98,7 +98,7 @@ export default function OrganizationsPage() {
const handleAcceptInvitation = async (invitationId: string, organizationId: string) => {
setIsAccepting(true);
try {
const response = await fetch('/auth/invitations/accept', {
const response = await fetch('/connect/invitations/accept', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -281,7 +281,7 @@ function OrganizationCard({ organization }: OrganizationCardProps) {
};
// Check if the URL is a backend SAML endpoint
const isSAMLUrl = targetUrl.includes('/auth/saml/');
const isSAMLUrl = targetUrl.includes('/connect/saml/');
return (
<Card padded className="w-full">

View File

@@ -111,7 +111,7 @@ export default function ConfirmEmailPage() {
<p className="text-green-600 dark:text-green-400">
{__("Your email has been confirmed successfully!")}
</p>
<Button onClick={() => navigate("/authentication/login")} className="w-full">
<Button onClick={() => navigate("/auth/login")} className="w-full">
{__("Proceed to Login")}
</Button>
</div>
@@ -141,7 +141,7 @@ export default function ConfirmEmailPage() {
{!isConfirmed && (
<p className="text-sm text-txt-tertiary">
<Link
to="/authentication/login"
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Back to Login")}

View File

@@ -23,7 +23,7 @@ export default function ForgotPasswordPage() {
const onSubmit = handleSubmit(async (data) => {
const response = await fetch(
buildEndpoint("/auth/forget-password"),
buildEndpoint("/connect/forget-password"),
{
method: "POST",
headers: {
@@ -81,7 +81,7 @@ export default function ForgotPasswordPage() {
<p className="text-sm text-txt-tertiary">
{__("Remember your password?")}{" "}
<Link
to="/authentication/login"
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Back to login")}
@@ -124,7 +124,7 @@ export default function ForgotPasswordPage() {
<p className="text-sm text-txt-tertiary">
{__("Remember your password?")}{" "}
<Link
to="/authentication/login"
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Back to login")}

View File

@@ -33,7 +33,7 @@ export default function LoginPage() {
setIsLoading(true);
try {
const res = await fetch(buildEndpoint("/auth/login"), {
const res = await fetch(buildEndpoint("/connect/login"), {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -68,7 +68,7 @@ export default function LoginPage() {
setIsChecking(true);
try {
const res = await fetch(buildEndpoint("/auth/check-sso"), {
const res = await fetch(buildEndpoint("/connect/check-sso"), {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -85,7 +85,7 @@ export default function LoginPage() {
if (data.ssoAvailable && data.samlConfigId) {
window.location.href = buildEndpoint(
`/auth/saml/login/${data.samlConfigId}`
`/connect/saml/login/${data.samlConfigId}`
);
} else {
throw new Error(__("SSO not available for this email domain"));
@@ -146,7 +146,7 @@ export default function LoginPage() {
<div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "}
<Link to="/authentication/register" className="underline hover:text-txt-primary">
<Link to="/auth/register" className="underline hover:text-txt-primary">
{__("Register")}
</Link>
</div>
@@ -154,7 +154,7 @@ export default function LoginPage() {
<div className="text-center text-sm text-txt-secondary">
{__("Forgot password?")}{" "}
<Link
to="/authentication/forgot-password"
to="/auth/forgot-password"
className="underline hover:text-txt-primary"
>
{__("Reset password")}
@@ -206,7 +206,7 @@ export default function LoginPage() {
<div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "}
<Link to="/authentication/register" className="underline hover:text-txt-primary">
<Link to="/auth/register" className="underline hover:text-txt-primary">
{__("Register")}
</Link>
</div>
@@ -214,7 +214,7 @@ export default function LoginPage() {
<div className="text-center text-sm text-txt-secondary">
{__("Forgot password?")}{" "}
<Link
to="/authentication/forgot-password"
to="/auth/forgot-password"
className="underline hover:text-txt-primary"
>
{__("Reset password")}
@@ -257,7 +257,7 @@ export default function LoginPage() {
<div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "}
<Link to="/authentication/register" className="underline hover:text-txt-primary">
<Link to="/auth/register" className="underline hover:text-txt-primary">
{__("Register")}
</Link>
</div>

View File

@@ -26,7 +26,7 @@ export default function RegisterPage() {
const onSubmit = handleSubmit(async (data) => {
const response = await fetch(
buildEndpoint("/auth/register"),
buildEndpoint("/connect/register"),
{
method: "POST",
headers: {
@@ -106,7 +106,7 @@ export default function RegisterPage() {
<p className="text-sm text-txt-tertiary">
{__("Already have an account?")}{" "}
<Link
to="/authentication/login"
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Log in here")}

View File

@@ -44,7 +44,7 @@ export default function ResetPasswordPage() {
}
const response = await fetch(
buildEndpoint("/auth/reset-password"),
buildEndpoint("/connect/reset-password"),
{
method: "POST",
headers: {
@@ -74,7 +74,7 @@ export default function ResetPasswordPage() {
description: __("Password reset successfully"),
variant: "success",
});
navigate("/authentication/login", { replace: true });
navigate("/auth/login", { replace: true });
});
usePageTitle(__("Reset password"));
@@ -118,7 +118,7 @@ export default function ResetPasswordPage() {
<p className="text-sm text-txt-tertiary">
{__("Remember your password?")}{" "}
<Link
to="/authentication/login"
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Log in here")}

View File

@@ -51,7 +51,7 @@ export default function SignupFromInvitationPage() {
}
const response = await fetch(
buildEndpoint("/auth/signup-from-invitation"),
buildEndpoint("/connect/signup-from-invitation"),
{
method: "POST",
headers: {
@@ -125,7 +125,7 @@ export default function SignupFromInvitationPage() {
<p className="text-sm text-txt-tertiary">
{__("Already have an account?")}{" "}
<Link
to="/authentication/login"
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Log in here")}

View File

@@ -50,7 +50,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
const error = useRouteError() ?? propsError;
if (error instanceof UnAuthenticatedError) {
return <Navigate to="/authentication/login" />;
return <Navigate to="/auth/login" />;
}
return <PageError error={error?.toString()} />;
@@ -58,7 +58,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
const routes = [
{
path: "/authentication",
path: "/auth",
Component: AuthLayout,
children: [
{

View File

@@ -9,7 +9,7 @@ unit:
max-queue-size: 2048
probod:
hostname: "https://gearnode.probo.engineering"
base-url: "https://gearnode.probo.engineering"
encryption-key: "thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA="
chrome-dp-addr: "localhost:9222"

View File

@@ -23,7 +23,7 @@ unit:
max-queue-size: 10000
probod:
hostname: "localhost:8080"
base-url: "http://localhost:8080"
encryption-key: "base64-encoded-encryption-key"
chrome-dp-addr: "localhost:9222"
@@ -249,11 +249,11 @@ Probod provides automatic structured JSON logging with:
### General Settings
#### `hostname` (string)
#### `base-url` (string)
**Default**: `"localhost:8080"`
**Default**: `"http://localhost:8080"`
The hostname and port where the Probod service will be accessible externally. This setting affects URL generation for redirects and API responses.
The base URL where the Probod service will be accessible externally. This should include the scheme (http or https), hostname, and optionally port. This setting affects URL generation for emails, redirects, and API responses. For production deployments, use the full HTTPS URL (e.g., `"https://app.example.com"`).
#### `encryption-key` (string)

View File

@@ -28,7 +28,7 @@ import (
var Templates embed.FS
const (
logoURLFormat = "https://%s/logos/probo.png"
logoURLPath = "/logos/probo.png"
subjectConfirmEmail = "Confirm your email address"
subjectPasswordReset = "Reset your password"
@@ -56,7 +56,7 @@ var (
trustCenterAccessTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/trust-center-access.txt.tmpl"))
)
func RenderConfirmEmail(hostname, fullName, confirmationUrl string) (subject string, textBody string, htmlBody *string, err error) {
func RenderConfirmEmail(baseURL, fullName, confirmationUrl string) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
ConfirmationUrl string
@@ -64,14 +64,14 @@ func RenderConfirmEmail(hostname, fullName, confirmationUrl string) (subject str
}{
FullName: fullName,
ConfirmationUrl: confirmationUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
}
textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data)
return subjectConfirmEmail, textBody, htmlBody, err
}
func RenderPasswordReset(hostname, fullName, resetUrl string) (subject string, textBody string, htmlBody *string, err error) {
func RenderPasswordReset(baseURL, fullName, resetUrl string) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
ResetUrl string
@@ -79,14 +79,14 @@ func RenderPasswordReset(hostname, fullName, resetUrl string) (subject string, t
}{
FullName: fullName,
ResetUrl: resetUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
}
textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data)
return subjectPasswordReset, textBody, htmlBody, err
}
func RenderInvitation(hostname, fullName, organizationName, invitationUrl string) (subject string, textBody string, htmlBody *string, err error) {
func RenderInvitation(baseURL, fullName, organizationName, invitationUrl string) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
OrganizationName string
@@ -96,14 +96,14 @@ func RenderInvitation(hostname, fullName, organizationName, invitationUrl string
FullName: fullName,
OrganizationName: organizationName,
InvitationUrl: invitationUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
}
textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data)
return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err
}
func RenderDocumentSigning(hostname, fullName, organizationName, signingUrl string) (subject string, textBody string, htmlBody *string, err error) {
func RenderDocumentSigning(baseURL, fullName, organizationName, signingUrl string) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
OrganizationName string
@@ -113,14 +113,14 @@ func RenderDocumentSigning(hostname, fullName, organizationName, signingUrl stri
FullName: fullName,
OrganizationName: organizationName,
SigningUrl: signingUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
}
textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data)
return fmt.Sprintf(subjectDocumentSigning, organizationName), textBody, htmlBody, err
}
func RenderDocumentExport(hostname, fullName, downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
func RenderDocumentExport(baseURL, fullName, downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
DownloadUrl string
@@ -128,14 +128,14 @@ func RenderDocumentExport(hostname, fullName, downloadUrl string) (subject strin
}{
FullName: fullName,
DownloadUrl: downloadUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
}
textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data)
return subjectDocumentExport, textBody, htmlBody, err
}
func RenderFrameworkExport(hostname, fullName, downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
func RenderFrameworkExport(baseURL, fullName, downloadUrl string) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
DownloadUrl string
@@ -143,14 +143,14 @@ func RenderFrameworkExport(hostname, fullName, downloadUrl string) (subject stri
}{
FullName: fullName,
DownloadUrl: downloadUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
}
textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data)
return subjectFrameworkExport, textBody, htmlBody, err
}
func RenderTrustCenterAccess(hostname, fullName, organizationName, accessUrl string, tokenDuration time.Duration) (subject string, textBody string, htmlBody *string, err error) {
func RenderTrustCenterAccess(baseURL, fullName, organizationName, accessUrl string, tokenDuration time.Duration) (subject string, textBody string, htmlBody *string, err error) {
durationInDays := int(math.Round(tokenDuration.Hours() / 24))
data := struct {
@@ -163,7 +163,7 @@ func RenderTrustCenterAccess(hostname, fullName, organizationName, accessUrl str
FullName: fullName,
OrganizationName: organizationName,
AccessUrl: accessUrl,
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
LogoURL: baseURL + logoURLPath,
DurationInDays: durationInDays,
}

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,