Refactor hostname to become baseurl
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -192,15 +192,16 @@ export function MainLayout() {
|
|||||||
function UserDropdown({ organizationId }: { organizationId: string }) {
|
function UserDropdown({ organizationId }: { organizationId: string }) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId }).viewer
|
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
|
||||||
.user;
|
organizationId,
|
||||||
|
}).viewer.user;
|
||||||
|
|
||||||
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (
|
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = async (
|
||||||
e
|
e
|
||||||
) => {
|
) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
fetch(buildEndpoint("/auth/logout"), {
|
fetch(buildEndpoint("/connect/logout"), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -274,13 +275,19 @@ interface InvitationsResponse {
|
|||||||
invitations: Invitation[];
|
invitations: Invitation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) {
|
function OrganizationSelectorWrapper({
|
||||||
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId });
|
organizationId,
|
||||||
|
}: {
|
||||||
|
organizationId: string;
|
||||||
|
}) {
|
||||||
|
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
|
||||||
|
organizationId,
|
||||||
|
});
|
||||||
return <OrganizationSelector currentOrganization={data.organization} />;
|
return <OrganizationSelector currentOrganization={data.organization} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrganizationSelector({
|
function OrganizationSelector({
|
||||||
currentOrganization
|
currentOrganization,
|
||||||
}: {
|
}: {
|
||||||
currentOrganization: MainLayoutQueryType["response"]["organization"];
|
currentOrganization: MainLayoutQueryType["response"]["organization"];
|
||||||
}) {
|
}) {
|
||||||
@@ -297,32 +304,32 @@ function OrganizationSelector({
|
|||||||
|
|
||||||
// Fetch organizations and invitations in parallel
|
// Fetch organizations and invitations in parallel
|
||||||
const [orgsResponse, invitationsResponse] = await Promise.all([
|
const [orgsResponse, invitationsResponse] = await Promise.all([
|
||||||
fetch('/auth/organizations', { credentials: 'include' }),
|
fetch("/connect/organizations", { credentials: "include" }),
|
||||||
fetch('/auth/invitations', { credentials: 'include' })
|
fetch("/connect/invitations", { credentials: "include" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!orgsResponse.ok) {
|
if (!orgsResponse.ok) {
|
||||||
throw new Error('Failed to fetch organizations');
|
throw new Error("Failed to fetch organizations");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invitationsResponse.ok) {
|
if (!invitationsResponse.ok) {
|
||||||
throw new Error('Failed to fetch invitations');
|
throw new Error("Failed to fetch invitations");
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgsData: OrganizationsResponse = await orgsResponse.json();
|
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(
|
const pendingCount = invitationsData.invitations.filter(
|
||||||
inv => !inv.acceptedAt
|
(inv) => !inv.acceptedAt
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
setOrganizations(orgsData.organizations);
|
setOrganizations(orgsData.organizations);
|
||||||
setPendingInvitationsCount(pendingCount);
|
setPendingInvitationsCount(pendingCount);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
setError(err instanceof Error ? err.message : "Unknown error");
|
||||||
console.error('Failed to fetch data:', err);
|
console.error("Failed to fetch data:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -334,11 +341,7 @@ function OrganizationSelector({
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button className="-ml-3" variant="tertiary" disabled>
|
||||||
className="-ml-3"
|
|
||||||
variant="tertiary"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{__("Error loading organizations")}
|
{__("Error loading organizations")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -355,7 +358,7 @@ function OrganizationSelector({
|
|||||||
iconAfter={IconChevronGrabberVertical}
|
iconAfter={IconChevronGrabberVertical}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? __("Loading...") : (currentOrganization?.name || "")}
|
{isLoading ? __("Loading...") : currentOrganization?.name || ""}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -370,7 +373,8 @@ function OrganizationSelector({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
organizations.map((organization) => {
|
organizations.map((organization) => {
|
||||||
const isAuthenticated = organization.authStatus === "authenticated";
|
const isAuthenticated =
|
||||||
|
organization.authStatus === "authenticated";
|
||||||
const isExpired = organization.authStatus === "expired";
|
const isExpired = organization.authStatus === "expired";
|
||||||
const needsAuth = organization.authStatus === "unauthenticated";
|
const needsAuth = organization.authStatus === "unauthenticated";
|
||||||
|
|
||||||
@@ -378,16 +382,16 @@ function OrganizationSelector({
|
|||||||
? `/organizations/${organization.id}`
|
? `/organizations/${organization.id}`
|
||||||
: organization.loginUrl;
|
: organization.loginUrl;
|
||||||
|
|
||||||
const isSAMLUrl = targetUrl.includes('/auth/saml/');
|
const isSAMLUrl = targetUrl.includes("/connect/saml/");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<DropdownItem asChild key={organization.id}>
|
||||||
asChild
|
|
||||||
key={organization.id}
|
|
||||||
>
|
|
||||||
{isSAMLUrl ? (
|
{isSAMLUrl ? (
|
||||||
<a href={targetUrl} className="flex items-center gap-2">
|
<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>
|
<span className="flex-1">{organization.name}</span>
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
<IconCheckmark1 size={16} className="text-green-600" />
|
||||||
@@ -401,7 +405,10 @@ function OrganizationSelector({
|
|||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<Link to={targetUrl} className="flex items-center gap-2">
|
<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>
|
<span className="flex-1">{organization.name}</span>
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<IconCheckmark1 size={16} className="text-green-600" />
|
<IconCheckmark1 size={16} className="text-green-600" />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function OrganizationsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOrganizations = async () => {
|
const fetchOrganizations = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/auth/organizations', {
|
const response = await fetch('/connect/organizations', {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export default function OrganizationsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchInvitations = async () => {
|
const fetchInvitations = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/auth/invitations', {
|
const response = await fetch('/connect/invitations', {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export default function OrganizationsPage() {
|
|||||||
const handleAcceptInvitation = async (invitationId: string, organizationId: string) => {
|
const handleAcceptInvitation = async (invitationId: string, organizationId: string) => {
|
||||||
setIsAccepting(true);
|
setIsAccepting(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/auth/invitations/accept', {
|
const response = await fetch('/connect/invitations/accept', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -281,7 +281,7 @@ function OrganizationCard({ organization }: OrganizationCardProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if the URL is a backend SAML endpoint
|
// Check if the URL is a backend SAML endpoint
|
||||||
const isSAMLUrl = targetUrl.includes('/auth/saml/');
|
const isSAMLUrl = targetUrl.includes('/connect/saml/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padded className="w-full">
|
<Card padded className="w-full">
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function ConfirmEmailPage() {
|
|||||||
<p className="text-green-600 dark:text-green-400">
|
<p className="text-green-600 dark:text-green-400">
|
||||||
{__("Your email has been confirmed successfully!")}
|
{__("Your email has been confirmed successfully!")}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => navigate("/authentication/login")} className="w-full">
|
<Button onClick={() => navigate("/auth/login")} className="w-full">
|
||||||
{__("Proceed to Login")}
|
{__("Proceed to Login")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +141,7 @@ export default function ConfirmEmailPage() {
|
|||||||
{!isConfirmed && (
|
{!isConfirmed && (
|
||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Back to Login")}
|
{__("Back to Login")}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function ForgotPasswordPage() {
|
|||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
buildEndpoint("/auth/forget-password"),
|
buildEndpoint("/connect/forget-password"),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -81,7 +81,7 @@ export default function ForgotPasswordPage() {
|
|||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Remember your password?")}{" "}
|
{__("Remember your password?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Back to login")}
|
{__("Back to login")}
|
||||||
@@ -124,7 +124,7 @@ export default function ForgotPasswordPage() {
|
|||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Remember your password?")}{" "}
|
{__("Remember your password?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Back to login")}
|
{__("Back to login")}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function LoginPage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(buildEndpoint("/auth/login"), {
|
const res = await fetch(buildEndpoint("/connect/login"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -68,7 +68,7 @@ export default function LoginPage() {
|
|||||||
setIsChecking(true);
|
setIsChecking(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(buildEndpoint("/auth/check-sso"), {
|
const res = await fetch(buildEndpoint("/connect/check-sso"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -85,7 +85,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
if (data.ssoAvailable && data.samlConfigId) {
|
if (data.ssoAvailable && data.samlConfigId) {
|
||||||
window.location.href = buildEndpoint(
|
window.location.href = buildEndpoint(
|
||||||
`/auth/saml/login/${data.samlConfigId}`
|
`/connect/saml/login/${data.samlConfigId}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(__("SSO not available for this email domain"));
|
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">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
{__("Don't have an account ?")}{" "}
|
{__("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")}
|
{__("Register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +154,7 @@ export default function LoginPage() {
|
|||||||
<div className="text-center text-sm text-txt-secondary">
|
<div className="text-center text-sm text-txt-secondary">
|
||||||
{__("Forgot password?")}{" "}
|
{__("Forgot password?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/forgot-password"
|
to="/auth/forgot-password"
|
||||||
className="underline hover:text-txt-primary"
|
className="underline hover:text-txt-primary"
|
||||||
>
|
>
|
||||||
{__("Reset password")}
|
{__("Reset password")}
|
||||||
@@ -206,7 +206,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
{__("Don't have an account ?")}{" "}
|
{__("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")}
|
{__("Register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +214,7 @@ export default function LoginPage() {
|
|||||||
<div className="text-center text-sm text-txt-secondary">
|
<div className="text-center text-sm text-txt-secondary">
|
||||||
{__("Forgot password?")}{" "}
|
{__("Forgot password?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/forgot-password"
|
to="/auth/forgot-password"
|
||||||
className="underline hover:text-txt-primary"
|
className="underline hover:text-txt-primary"
|
||||||
>
|
>
|
||||||
{__("Reset password")}
|
{__("Reset password")}
|
||||||
@@ -257,7 +257,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<div className="text-center mt-6 text-sm text-txt-secondary">
|
<div className="text-center mt-6 text-sm text-txt-secondary">
|
||||||
{__("Don't have an account ?")}{" "}
|
{__("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")}
|
{__("Register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function RegisterPage() {
|
|||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
buildEndpoint("/auth/register"),
|
buildEndpoint("/connect/register"),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -106,7 +106,7 @@ export default function RegisterPage() {
|
|||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Already have an account?")}{" "}
|
{__("Already have an account?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Log in here")}
|
{__("Log in here")}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function ResetPasswordPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
buildEndpoint("/auth/reset-password"),
|
buildEndpoint("/connect/reset-password"),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -74,7 +74,7 @@ export default function ResetPasswordPage() {
|
|||||||
description: __("Password reset successfully"),
|
description: __("Password reset successfully"),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
});
|
});
|
||||||
navigate("/authentication/login", { replace: true });
|
navigate("/auth/login", { replace: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
usePageTitle(__("Reset password"));
|
usePageTitle(__("Reset password"));
|
||||||
@@ -118,7 +118,7 @@ export default function ResetPasswordPage() {
|
|||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Remember your password?")}{" "}
|
{__("Remember your password?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Log in here")}
|
{__("Log in here")}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function SignupFromInvitationPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
buildEndpoint("/auth/signup-from-invitation"),
|
buildEndpoint("/connect/signup-from-invitation"),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -125,7 +125,7 @@ export default function SignupFromInvitationPage() {
|
|||||||
<p className="text-sm text-txt-tertiary">
|
<p className="text-sm text-txt-tertiary">
|
||||||
{__("Already have an account?")}{" "}
|
{__("Already have an account?")}{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/authentication/login"
|
to="/auth/login"
|
||||||
className="underline text-txt-primary hover:text-txt-secondary"
|
className="underline text-txt-primary hover:text-txt-secondary"
|
||||||
>
|
>
|
||||||
{__("Log in here")}
|
{__("Log in here")}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
|
|||||||
const error = useRouteError() ?? propsError;
|
const error = useRouteError() ?? propsError;
|
||||||
|
|
||||||
if (error instanceof UnAuthenticatedError) {
|
if (error instanceof UnAuthenticatedError) {
|
||||||
return <Navigate to="/authentication/login" />;
|
return <Navigate to="/auth/login" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <PageError error={error?.toString()} />;
|
return <PageError error={error?.toString()} />;
|
||||||
@@ -58,7 +58,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
|
|||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/authentication",
|
path: "/auth",
|
||||||
Component: AuthLayout,
|
Component: AuthLayout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ unit:
|
|||||||
max-queue-size: 2048
|
max-queue-size: 2048
|
||||||
|
|
||||||
probod:
|
probod:
|
||||||
hostname: "https://gearnode.probo.engineering"
|
base-url: "https://gearnode.probo.engineering"
|
||||||
encryption-key: "thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
encryption-key: "thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||||
chrome-dp-addr: "localhost:9222"
|
chrome-dp-addr: "localhost:9222"
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ unit:
|
|||||||
max-queue-size: 10000
|
max-queue-size: 10000
|
||||||
|
|
||||||
probod:
|
probod:
|
||||||
hostname: "localhost:8080"
|
base-url: "http://localhost:8080"
|
||||||
encryption-key: "base64-encoded-encryption-key"
|
encryption-key: "base64-encoded-encryption-key"
|
||||||
chrome-dp-addr: "localhost:9222"
|
chrome-dp-addr: "localhost:9222"
|
||||||
|
|
||||||
@@ -249,11 +249,11 @@ Probod provides automatic structured JSON logging with:
|
|||||||
|
|
||||||
### General Settings
|
### 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)
|
#### `encryption-key` (string)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import (
|
|||||||
var Templates embed.FS
|
var Templates embed.FS
|
||||||
|
|
||||||
const (
|
const (
|
||||||
logoURLFormat = "https://%s/logos/probo.png"
|
logoURLPath = "/logos/probo.png"
|
||||||
|
|
||||||
subjectConfirmEmail = "Confirm your email address"
|
subjectConfirmEmail = "Confirm your email address"
|
||||||
subjectPasswordReset = "Reset your password"
|
subjectPasswordReset = "Reset your password"
|
||||||
@@ -56,7 +56,7 @@ var (
|
|||||||
trustCenterAccessTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/trust-center-access.txt.tmpl"))
|
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 {
|
data := struct {
|
||||||
FullName string
|
FullName string
|
||||||
ConfirmationUrl string
|
ConfirmationUrl string
|
||||||
@@ -64,14 +64,14 @@ func RenderConfirmEmail(hostname, fullName, confirmationUrl string) (subject str
|
|||||||
}{
|
}{
|
||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
ConfirmationUrl: confirmationUrl,
|
ConfirmationUrl: confirmationUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data)
|
||||||
return subjectConfirmEmail, textBody, htmlBody, err
|
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 {
|
data := struct {
|
||||||
FullName string
|
FullName string
|
||||||
ResetUrl string
|
ResetUrl string
|
||||||
@@ -79,14 +79,14 @@ func RenderPasswordReset(hostname, fullName, resetUrl string) (subject string, t
|
|||||||
}{
|
}{
|
||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
ResetUrl: resetUrl,
|
ResetUrl: resetUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data)
|
||||||
return subjectPasswordReset, textBody, htmlBody, err
|
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 {
|
data := struct {
|
||||||
FullName string
|
FullName string
|
||||||
OrganizationName string
|
OrganizationName string
|
||||||
@@ -96,14 +96,14 @@ func RenderInvitation(hostname, fullName, organizationName, invitationUrl string
|
|||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
OrganizationName: organizationName,
|
OrganizationName: organizationName,
|
||||||
InvitationUrl: invitationUrl,
|
InvitationUrl: invitationUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data)
|
||||||
return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err
|
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 {
|
data := struct {
|
||||||
FullName string
|
FullName string
|
||||||
OrganizationName string
|
OrganizationName string
|
||||||
@@ -113,14 +113,14 @@ func RenderDocumentSigning(hostname, fullName, organizationName, signingUrl stri
|
|||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
OrganizationName: organizationName,
|
OrganizationName: organizationName,
|
||||||
SigningUrl: signingUrl,
|
SigningUrl: signingUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data)
|
||||||
return fmt.Sprintf(subjectDocumentSigning, organizationName), textBody, htmlBody, err
|
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 {
|
data := struct {
|
||||||
FullName string
|
FullName string
|
||||||
DownloadUrl string
|
DownloadUrl string
|
||||||
@@ -128,14 +128,14 @@ func RenderDocumentExport(hostname, fullName, downloadUrl string) (subject strin
|
|||||||
}{
|
}{
|
||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
DownloadUrl: downloadUrl,
|
DownloadUrl: downloadUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data)
|
||||||
return subjectDocumentExport, textBody, htmlBody, err
|
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 {
|
data := struct {
|
||||||
FullName string
|
FullName string
|
||||||
DownloadUrl string
|
DownloadUrl string
|
||||||
@@ -143,14 +143,14 @@ func RenderFrameworkExport(hostname, fullName, downloadUrl string) (subject stri
|
|||||||
}{
|
}{
|
||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
DownloadUrl: downloadUrl,
|
DownloadUrl: downloadUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data)
|
textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data)
|
||||||
return subjectFrameworkExport, textBody, htmlBody, err
|
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))
|
durationInDays := int(math.Round(tokenDuration.Hours() / 24))
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
@@ -163,7 +163,7 @@ func RenderTrustCenterAccess(hostname, fullName, organizationName, accessUrl str
|
|||||||
FullName: fullName,
|
FullName: fullName,
|
||||||
OrganizationName: organizationName,
|
OrganizationName: organizationName,
|
||||||
AccessUrl: accessUrl,
|
AccessUrl: accessUrl,
|
||||||
LogoURL: fmt.Sprintf(logoURLFormat, hostname),
|
LogoURL: baseURL + logoURLPath,
|
||||||
DurationInDays: durationInDays,
|
DurationInDays: durationInDays,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,13 +74,13 @@ func (r AccessResult) ToError(baseURL string) error {
|
|||||||
case AuthMethodPassword:
|
case AuthMethodPassword:
|
||||||
return ErrPasswordAuthRequired{
|
return ErrPasswordAuthRequired{
|
||||||
OrganizationID: r.OrganizationID,
|
OrganizationID: r.OrganizationID,
|
||||||
RedirectURL: fmt.Sprintf("%s/authentication/login?method=password", baseURL),
|
RedirectURL: fmt.Sprintf("%s/auth/login?method=password", baseURL),
|
||||||
}
|
}
|
||||||
case AuthMethodSAML, AuthMethodAny:
|
case AuthMethodSAML, AuthMethodAny:
|
||||||
return ErrSAMLAuthRequired{
|
return ErrSAMLAuthRequired{
|
||||||
ConfigID: r.SAMLConfig.ID,
|
ConfigID: r.SAMLConfig.ID,
|
||||||
OrganizationID: r.OrganizationID,
|
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:
|
default:
|
||||||
return fmt.Errorf("access denied to organization %s", r.OrganizationID)
|
return fmt.Errorf("access denied to organization %s", r.OrganizationID)
|
||||||
|
|||||||
@@ -224,11 +224,11 @@ func NewSAMLService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SAMLService) GetEntityID() string {
|
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 {
|
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) {
|
func parseRawSAMLResponse(encodedResponse string) (*saml.Assertion, error) {
|
||||||
@@ -564,7 +564,7 @@ func (s *SAMLService) HandleSAMLAssertion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SAMLService) GetMetadataURL(organizationID gid.GID) string {
|
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) {
|
func (s *SAMLService) GenerateMetadata() ([]byte, error) {
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getprobo/probo/packages/emails"
|
"github.com/getprobo/probo/packages/emails"
|
||||||
|
"github.com/getprobo/probo/pkg/baseurl"
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
"github.com/getprobo/probo/pkg/crypto/cipher"
|
"github.com/getprobo/probo/pkg/crypto/cipher"
|
||||||
"github.com/getprobo/probo/pkg/crypto/passwdhash"
|
"github.com/getprobo/probo/pkg/crypto/passwdhash"
|
||||||
@@ -40,7 +40,6 @@ type (
|
|||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
hp *passwdhash.Profile
|
hp *passwdhash.Profile
|
||||||
hostname string
|
|
||||||
baseURL string
|
baseURL string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
disableSignup bool
|
disableSignup bool
|
||||||
@@ -51,7 +50,6 @@ type (
|
|||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
hp *passwdhash.Profile
|
hp *passwdhash.Profile
|
||||||
hostname string
|
|
||||||
baseURL string
|
baseURL string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
scope coredata.Scoper
|
scope coredata.Scoper
|
||||||
@@ -187,7 +185,6 @@ func NewService(
|
|||||||
encryptionKey cipher.EncryptionKey,
|
encryptionKey cipher.EncryptionKey,
|
||||||
hp *passwdhash.Profile,
|
hp *passwdhash.Profile,
|
||||||
tokenSecret string,
|
tokenSecret string,
|
||||||
hostname string,
|
|
||||||
baseURL string,
|
baseURL string,
|
||||||
disableSignup bool,
|
disableSignup bool,
|
||||||
invitationTokenValidity time.Duration,
|
invitationTokenValidity time.Duration,
|
||||||
@@ -196,7 +193,6 @@ func NewService(
|
|||||||
pg: pgClient,
|
pg: pgClient,
|
||||||
encryptionKey: encryptionKey,
|
encryptionKey: encryptionKey,
|
||||||
hp: hp,
|
hp: hp,
|
||||||
hostname: hostname,
|
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
tokenSecret: tokenSecret,
|
tokenSecret: tokenSecret,
|
||||||
disableSignup: disableSignup,
|
disableSignup: disableSignup,
|
||||||
@@ -209,7 +205,6 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthService {
|
|||||||
pg: s.pg,
|
pg: s.pg,
|
||||||
encryptionKey: s.encryptionKey,
|
encryptionKey: s.encryptionKey,
|
||||||
hp: s.hp,
|
hp: s.hp,
|
||||||
hostname: s.hostname,
|
|
||||||
baseURL: s.baseURL,
|
baseURL: s.baseURL,
|
||||||
tokenSecret: s.tokenSecret,
|
tokenSecret: s.tokenSecret,
|
||||||
scope: coredata.NewScope(tenantID),
|
scope: coredata.NewScope(tenantID),
|
||||||
@@ -232,13 +227,17 @@ func (s Service) ForgetPassword(
|
|||||||
return fmt.Errorf("cannot generate password reset token: %w", err)
|
return fmt.Errorf("cannot generate password reset token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPasswordUrl := url.URL{
|
base, err := baseurl.Parse(s.baseURL)
|
||||||
Scheme: "https",
|
if err != nil {
|
||||||
Host: s.hostname,
|
return fmt.Errorf("cannot parse base URL: %w", err)
|
||||||
Path: "/auth/reset-password",
|
}
|
||||||
RawQuery: url.Values{
|
|
||||||
"token": []string{passwordResetToken},
|
resetPasswordUrl, err := base.
|
||||||
}.Encode(),
|
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(
|
return s.pg.WithConn(
|
||||||
@@ -255,9 +254,9 @@ func (s Service) ForgetPassword(
|
|||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderPasswordReset(
|
subject, textBody, htmlBody, err := emails.RenderPasswordReset(
|
||||||
s.hostname,
|
s.baseURL,
|
||||||
user.FullName,
|
user.FullName,
|
||||||
resetPasswordUrl.String(),
|
resetPasswordUrl,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render password reset email: %w", err)
|
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)
|
return fmt.Errorf("cannot generate confirmation token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmationUrl := url.URL{
|
base, err := baseurl.Parse(s.baseURL)
|
||||||
Scheme: "https",
|
if err != nil {
|
||||||
Host: s.hostname,
|
return fmt.Errorf("cannot parse base URL: %w", err)
|
||||||
Path: "/auth/confirm-email",
|
}
|
||||||
RawQuery: url.Values{
|
|
||||||
"token": []string{confirmationToken},
|
confirmationUrl, err := base.
|
||||||
}.Encode(),
|
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(
|
subject, textBody, htmlBody, err := emails.RenderConfirmEmail(
|
||||||
s.hostname,
|
s.baseURL,
|
||||||
user.FullName,
|
user.FullName,
|
||||||
confirmationUrl.String(),
|
confirmationUrl,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot render confirmation email: %w", err)
|
return fmt.Errorf("cannot render confirmation email: %w", err)
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ import (
|
|||||||
type (
|
type (
|
||||||
Service struct {
|
Service struct {
|
||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
hostname string
|
baseURL string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
invitationTokenValidity time.Duration
|
invitationTokenValidity time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
TenantAuthzService struct {
|
TenantAuthzService struct {
|
||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
hostname string
|
baseURL string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
invitationTokenValidity time.Duration
|
invitationTokenValidity time.Duration
|
||||||
scope coredata.Scoper
|
scope coredata.Scoper
|
||||||
@@ -62,13 +62,13 @@ const (
|
|||||||
func NewService(
|
func NewService(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pgClient *pg.Client,
|
pgClient *pg.Client,
|
||||||
hostname string,
|
baseURL string,
|
||||||
tokenSecret string,
|
tokenSecret string,
|
||||||
invitationTokenValidity time.Duration,
|
invitationTokenValidity time.Duration,
|
||||||
) (*Service, error) {
|
) (*Service, error) {
|
||||||
return &Service{
|
return &Service{
|
||||||
pg: pgClient,
|
pg: pgClient,
|
||||||
hostname: hostname,
|
baseURL: baseURL,
|
||||||
tokenSecret: tokenSecret,
|
tokenSecret: tokenSecret,
|
||||||
invitationTokenValidity: invitationTokenValidity,
|
invitationTokenValidity: invitationTokenValidity,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -77,7 +77,7 @@ func NewService(
|
|||||||
func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthzService {
|
func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthzService {
|
||||||
return &TenantAuthzService{
|
return &TenantAuthzService{
|
||||||
pg: s.pg,
|
pg: s.pg,
|
||||||
hostname: s.hostname,
|
baseURL: s.baseURL,
|
||||||
tokenSecret: s.tokenSecret,
|
tokenSecret: s.tokenSecret,
|
||||||
invitationTokenValidity: s.invitationTokenValidity,
|
invitationTokenValidity: s.invitationTokenValidity,
|
||||||
scope: coredata.NewScope(tenantID),
|
scope: coredata.NewScope(tenantID),
|
||||||
@@ -743,7 +743,7 @@ func (s *TenantAuthzService) InviteUserToOrganization(
|
|||||||
|
|
||||||
if userExists {
|
if userExists {
|
||||||
recipientName = user.FullName
|
recipientName = user.FullName
|
||||||
invitationURL = fmt.Sprintf("https://%s/", s.hostname)
|
invitationURL = s.baseURL + "/"
|
||||||
} else {
|
} else {
|
||||||
recipientName = fullName
|
recipientName = fullName
|
||||||
invitationData := coredata.InvitationData{
|
invitationData := coredata.InvitationData{
|
||||||
@@ -764,11 +764,11 @@ func (s *TenantAuthzService) InviteUserToOrganization(
|
|||||||
return fmt.Errorf("cannot generate invitation token: %w", err)
|
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(
|
subject, textBody, htmlBody, err := emails.RenderInvitation(
|
||||||
s.hostname,
|
s.baseURL,
|
||||||
recipientName,
|
recipientName,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
invitationURL,
|
invitationURL,
|
||||||
|
|||||||
226
pkg/baseurl/baseurl.go
Normal file
226
pkg/baseurl/baseurl.go
Normal 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
243
pkg/baseurl/baseurl_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -457,9 +457,14 @@ func (s *DocumentService) SendSigningNotifications(
|
|||||||
return fmt.Errorf("cannot create signing request token: %w", err)
|
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{
|
signRequestURL := url.URL{
|
||||||
Scheme: "https",
|
Scheme: baseURLParsed.Scheme,
|
||||||
Host: s.svc.hostname,
|
Host: baseURLParsed.Host,
|
||||||
Path: "/documents/signing-requests",
|
Path: "/documents/signing-requests",
|
||||||
RawQuery: url.Values{
|
RawQuery: url.Values{
|
||||||
"token": []string{token},
|
"token": []string{token},
|
||||||
@@ -467,7 +472,7 @@ func (s *DocumentService) SendSigningNotifications(
|
|||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderDocumentSigning(
|
subject, textBody, htmlBody, err := emails.RenderDocumentSigning(
|
||||||
s.svc.hostname,
|
s.svc.baseURL,
|
||||||
people.FullName,
|
people.FullName,
|
||||||
organization.Name,
|
organization.Name,
|
||||||
signRequestURL.String(),
|
signRequestURL.String(),
|
||||||
@@ -1584,7 +1589,7 @@ func (s *DocumentService) SendExportEmail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderDocumentExport(
|
subject, textBody, htmlBody, err := emails.RenderDocumentExport(
|
||||||
s.svc.hostname,
|
s.svc.baseURL,
|
||||||
recipientName,
|
recipientName,
|
||||||
downloadURL,
|
downloadURL,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -709,7 +709,7 @@ func (s FrameworkService) SendExportEmail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
subject, textBody, htmlBody, err := emails.RenderFrameworkExport(
|
subject, textBody, htmlBody, err := emails.RenderFrameworkExport(
|
||||||
s.svc.hostname,
|
s.svc.baseURL,
|
||||||
recipientName,
|
recipientName,
|
||||||
downloadURL,
|
downloadURL,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type (
|
|||||||
s3 *s3.Client
|
s3 *s3.Client
|
||||||
bucket string
|
bucket string
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
hostname string
|
baseURL string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
trustConfig TrustConfig
|
trustConfig TrustConfig
|
||||||
agentConfig agents.Config
|
agentConfig agents.Config
|
||||||
@@ -70,7 +70,7 @@ type (
|
|||||||
bucket string
|
bucket string
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
scope coredata.Scoper
|
scope coredata.Scoper
|
||||||
hostname string
|
baseURL string
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
trustConfig TrustConfig
|
trustConfig TrustConfig
|
||||||
agent *agents.Agent
|
agent *agents.Agent
|
||||||
@@ -115,7 +115,7 @@ func NewService(
|
|||||||
pgClient *pg.Client,
|
pgClient *pg.Client,
|
||||||
s3Client *s3.Client,
|
s3Client *s3.Client,
|
||||||
bucket string,
|
bucket string,
|
||||||
hostname string,
|
baseURL string,
|
||||||
tokenSecret string,
|
tokenSecret string,
|
||||||
trustConfig TrustConfig,
|
trustConfig TrustConfig,
|
||||||
agentConfig agents.Config,
|
agentConfig agents.Config,
|
||||||
@@ -135,7 +135,7 @@ func NewService(
|
|||||||
s3: s3Client,
|
s3: s3Client,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
encryptionKey: encryptionKey,
|
encryptionKey: encryptionKey,
|
||||||
hostname: hostname,
|
baseURL: baseURL,
|
||||||
tokenSecret: tokenSecret,
|
tokenSecret: tokenSecret,
|
||||||
trustConfig: trustConfig,
|
trustConfig: trustConfig,
|
||||||
agentConfig: agentConfig,
|
agentConfig: agentConfig,
|
||||||
@@ -156,7 +156,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
|
|||||||
s3: s.s3,
|
s3: s.s3,
|
||||||
bucket: s.bucket,
|
bucket: s.bucket,
|
||||||
encryptionKey: s.encryptionKey,
|
encryptionKey: s.encryptionKey,
|
||||||
hostname: s.hostname,
|
baseURL: s.baseURL,
|
||||||
scope: coredata.NewScope(tenantID),
|
scope: coredata.NewScope(tenantID),
|
||||||
tokenSecret: s.tokenSecret,
|
tokenSecret: s.tokenSecret,
|
||||||
trustConfig: s.trustConfig,
|
trustConfig: s.trustConfig,
|
||||||
|
|||||||
@@ -442,7 +442,13 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
|
|||||||
return fmt.Errorf("cannot load organization: %w", err)
|
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"
|
path := "/trust/" + trustCenter.Slug + "/access"
|
||||||
|
|
||||||
if organization.CustomDomainID != nil {
|
if organization.CustomDomainID != nil {
|
||||||
@@ -456,11 +462,12 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
hostname = customDomain.Domain
|
hostname = customDomain.Domain
|
||||||
|
scheme = "https"
|
||||||
path = "/access"
|
path = "/access"
|
||||||
}
|
}
|
||||||
|
|
||||||
accessURL := url.URL{
|
accessURL := url.URL{
|
||||||
Scheme: "https",
|
Scheme: scheme,
|
||||||
Host: hostname,
|
Host: hostname,
|
||||||
Path: path,
|
Path: path,
|
||||||
RawQuery: url.Values{
|
RawQuery: url.Values{
|
||||||
@@ -489,7 +496,7 @@ func (s TrustCenterAccessService) sendTrustCenterAccessEmail(
|
|||||||
accessURL string,
|
accessURL string,
|
||||||
) error {
|
) error {
|
||||||
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
|
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
|
||||||
s.svc.hostname,
|
s.svc.baseURL,
|
||||||
name,
|
name,
|
||||||
companyName,
|
companyName,
|
||||||
accessURL,
|
accessURL,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/getprobo/probo/pkg/auth"
|
"github.com/getprobo/probo/pkg/auth"
|
||||||
"github.com/getprobo/probo/pkg/authz"
|
"github.com/getprobo/probo/pkg/authz"
|
||||||
"github.com/getprobo/probo/pkg/awsconfig"
|
"github.com/getprobo/probo/pkg/awsconfig"
|
||||||
|
"github.com/getprobo/probo/pkg/baseurl"
|
||||||
"github.com/getprobo/probo/pkg/certmanager"
|
"github.com/getprobo/probo/pkg/certmanager"
|
||||||
"github.com/getprobo/probo/pkg/connector"
|
"github.com/getprobo/probo/pkg/connector"
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
@@ -64,7 +65,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
config struct {
|
config struct {
|
||||||
Hostname string `json:"hostname"`
|
BaseURL *baseurl.BaseURL `json:"base-url"`
|
||||||
EncryptionKey cipher.EncryptionKey `json:"encryption-key"`
|
EncryptionKey cipher.EncryptionKey `json:"encryption-key"`
|
||||||
Pg pgConfig `json:"pg"`
|
Pg pgConfig `json:"pg"`
|
||||||
Api apiConfig `json:"api"`
|
Api apiConfig `json:"api"`
|
||||||
@@ -93,7 +94,7 @@ var (
|
|||||||
func New() *Implm {
|
func New() *Implm {
|
||||||
return &Implm{
|
return &Implm{
|
||||||
cfg: config{
|
cfg: config{
|
||||||
Hostname: "localhost:8080",
|
BaseURL: baseurl.MustParse("http://localhost:8080"),
|
||||||
Api: apiConfig{
|
Api: apiConfig{
|
||||||
Addr: "localhost:8080",
|
Addr: "localhost:8080",
|
||||||
},
|
},
|
||||||
@@ -275,8 +276,7 @@ func (impl *Implm) Run(
|
|||||||
impl.cfg.EncryptionKey,
|
impl.cfg.EncryptionKey,
|
||||||
hp,
|
hp,
|
||||||
impl.cfg.Auth.Cookie.Secret,
|
impl.cfg.Auth.Cookie.Secret,
|
||||||
impl.cfg.Hostname,
|
impl.cfg.BaseURL.String(),
|
||||||
fmt.Sprintf("https://%s", impl.cfg.Hostname),
|
|
||||||
impl.cfg.Auth.DisableSignup,
|
impl.cfg.Auth.DisableSignup,
|
||||||
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
|
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
|
||||||
)
|
)
|
||||||
@@ -287,7 +287,7 @@ func (impl *Implm) Run(
|
|||||||
authzService, err := authz.NewService(
|
authzService, err := authz.NewService(
|
||||||
ctx,
|
ctx,
|
||||||
pgClient,
|
pgClient,
|
||||||
impl.cfg.Hostname,
|
impl.cfg.BaseURL.String(),
|
||||||
impl.cfg.Auth.Cookie.Secret,
|
impl.cfg.Auth.Cookie.Secret,
|
||||||
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
|
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
|
||||||
)
|
)
|
||||||
@@ -300,7 +300,7 @@ func (impl *Implm) Run(
|
|||||||
samlService, err := auth.NewSAMLService(
|
samlService, err := auth.NewSAMLService(
|
||||||
pgClient,
|
pgClient,
|
||||||
impl.cfg.EncryptionKey,
|
impl.cfg.EncryptionKey,
|
||||||
fmt.Sprintf("https://%s", impl.cfg.Hostname),
|
impl.cfg.BaseURL.String(),
|
||||||
impl.cfg.Auth.SAML.SessionDurationTime(),
|
impl.cfg.Auth.SAML.SessionDurationTime(),
|
||||||
impl.cfg.Auth.Cookie.Name,
|
impl.cfg.Auth.Cookie.Name,
|
||||||
impl.cfg.Auth.Cookie.Secret,
|
impl.cfg.Auth.Cookie.Secret,
|
||||||
@@ -347,7 +347,7 @@ func (impl *Implm) Run(
|
|||||||
pgClient,
|
pgClient,
|
||||||
s3Client,
|
s3Client,
|
||||||
impl.cfg.AWS.Bucket,
|
impl.cfg.AWS.Bucket,
|
||||||
impl.cfg.Hostname,
|
impl.cfg.BaseURL.String(),
|
||||||
impl.cfg.Auth.Cookie.Secret,
|
impl.cfg.Auth.Cookie.Secret,
|
||||||
trustConfig,
|
trustConfig,
|
||||||
agentConfig,
|
agentConfig,
|
||||||
@@ -366,7 +366,7 @@ func (impl *Implm) Run(
|
|||||||
pgClient,
|
pgClient,
|
||||||
s3Client,
|
s3Client,
|
||||||
impl.cfg.AWS.Bucket,
|
impl.cfg.AWS.Bucket,
|
||||||
impl.cfg.Hostname,
|
impl.cfg.BaseURL.String(),
|
||||||
impl.cfg.EncryptionKey,
|
impl.cfg.EncryptionKey,
|
||||||
impl.cfg.TrustAuth.TokenSecret,
|
impl.cfg.TrustAuth.TokenSecret,
|
||||||
impl.cfg.GetSlackSigningSecret(),
|
impl.cfg.GetSlackSigningSecret(),
|
||||||
@@ -392,7 +392,7 @@ func (impl *Implm) Run(
|
|||||||
SAML: samlService,
|
SAML: samlService,
|
||||||
ConnectorRegistry: defaultConnectorRegistry,
|
ConnectorRegistry: defaultConnectorRegistry,
|
||||||
Agent: agent,
|
Agent: agent,
|
||||||
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname},
|
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.BaseURL.Host()},
|
||||||
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
|
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
|
||||||
FileManager: fileManagerService,
|
FileManager: fileManagerService,
|
||||||
PGClient: pgClient,
|
PGClient: pgClient,
|
||||||
|
|||||||
@@ -4997,12 +4997,12 @@ func (r *sAMLConfigurationResolver) SpMetadataURL(ctx context.Context, obj *type
|
|||||||
// TestLoginURL is the resolver for the testLoginUrl field.
|
// TestLoginURL is the resolver for the testLoginUrl field.
|
||||||
func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
|
func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
|
||||||
entityID := r.samlSvc.GetEntityID()
|
entityID := r.samlSvc.GetEntityID()
|
||||||
parts := strings.Split(entityID, "/auth/saml/metadata")
|
parts := strings.Split(entityID, "/connect/saml/metadata")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return "", fmt.Errorf("invalid entity ID format")
|
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.
|
// Organization is the resolver for the organization field.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func buildOrganizationResponse(
|
|||||||
// Generate logo URL path if organization has a logo
|
// Generate logo URL path if organization has a logo
|
||||||
var logoURL *string
|
var logoURL *string
|
||||||
if org.LogoFileID != nil {
|
if org.LogoFileID != nil {
|
||||||
url := fmt.Sprintf("/auth/organizations/%s/logo", org.ID)
|
url := fmt.Sprintf("/connect/organizations/%s/logo", org.ID)
|
||||||
logoURL = &url
|
logoURL = &url
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +74,11 @@ func buildOrganizationResponse(
|
|||||||
case authsvc.AuthMethodSAML, authsvc.AuthMethodAny:
|
case authsvc.AuthMethodSAML, authsvc.AuthMethodAny:
|
||||||
orgResponse.AuthenticationMethod = "saml"
|
orgResponse.AuthenticationMethod = "saml"
|
||||||
if accessResult.SAMLConfig != nil {
|
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:
|
case authsvc.AuthMethodPassword:
|
||||||
orgResponse.AuthenticationMethod = "password"
|
orgResponse.AuthenticationMethod = "password"
|
||||||
orgResponse.LoginURL = "/authentication/login?method=password"
|
orgResponse.LoginURL = "/auth/login?method=password"
|
||||||
}
|
}
|
||||||
return orgResponse
|
return orgResponse
|
||||||
}
|
}
|
||||||
@@ -88,13 +88,13 @@ func buildOrganizationResponse(
|
|||||||
|
|
||||||
if sessionData.PasswordAuthenticated {
|
if sessionData.PasswordAuthenticated {
|
||||||
orgResponse.AuthenticationMethod = "password"
|
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 {
|
} else if samlInfo, ok := sessionData.SAMLAuthenticatedOrgs[org.ID.String()]; ok {
|
||||||
orgResponse.AuthenticationMethod = "saml"
|
orgResponse.AuthenticationMethod = "saml"
|
||||||
orgResponse.LoginURL = fmt.Sprintf("/auth/saml/login/%s", samlInfo.SAMLConfigID)
|
orgResponse.LoginURL = fmt.Sprintf("/connect/saml/login/%s", samlInfo.SAMLConfigID)
|
||||||
} else {
|
} else {
|
||||||
orgResponse.AuthenticationMethod = "any"
|
orgResponse.AuthenticationMethod = "any"
|
||||||
orgResponse.LoginURL = "/authentication/login?method=password"
|
orgResponse.LoginURL = "/auth/login?method=password"
|
||||||
}
|
}
|
||||||
|
|
||||||
return orgResponse
|
return orgResponse
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
|
|
||||||
func (s *Server) setupRoutes() {
|
func (s *Server) setupRoutes() {
|
||||||
s.router.Mount("/api", s.apiServer)
|
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) {
|
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
|
||||||
r.Use(s.loadTrustCenterBySlugOrID)
|
r.Use(s.loadTrustCenterBySlugOrID)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ type (
|
|||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
slackSigningSecret string
|
slackSigningSecret string
|
||||||
hostname string
|
baseURL string
|
||||||
auth *auth.Service
|
auth *auth.Service
|
||||||
html2pdfConverter *html2pdf.Converter
|
html2pdfConverter *html2pdf.Converter
|
||||||
fileManager *filemanager.Service
|
fileManager *filemanager.Service
|
||||||
@@ -61,7 +61,7 @@ type (
|
|||||||
proboSvc *probo.Service
|
proboSvc *probo.Service
|
||||||
encryptionKey cipher.EncryptionKey
|
encryptionKey cipher.EncryptionKey
|
||||||
tokenSecret string
|
tokenSecret string
|
||||||
hostname string
|
baseURL string
|
||||||
auth *auth.Service
|
auth *auth.Service
|
||||||
html2pdfConverter *html2pdf.Converter
|
html2pdfConverter *html2pdf.Converter
|
||||||
fileManager *filemanager.Service
|
fileManager *filemanager.Service
|
||||||
@@ -85,7 +85,7 @@ func NewService(
|
|||||||
pgClient *pg.Client,
|
pgClient *pg.Client,
|
||||||
s3Client *s3.Client,
|
s3Client *s3.Client,
|
||||||
bucket string,
|
bucket string,
|
||||||
hostname string,
|
baseURL string,
|
||||||
encryptionKey cipher.EncryptionKey,
|
encryptionKey cipher.EncryptionKey,
|
||||||
tokenSecret string,
|
tokenSecret string,
|
||||||
slackSigningSecret string,
|
slackSigningSecret string,
|
||||||
@@ -102,7 +102,7 @@ func NewService(
|
|||||||
encryptionKey: encryptionKey,
|
encryptionKey: encryptionKey,
|
||||||
tokenSecret: tokenSecret,
|
tokenSecret: tokenSecret,
|
||||||
slackSigningSecret: slackSigningSecret,
|
slackSigningSecret: slackSigningSecret,
|
||||||
hostname: hostname,
|
baseURL: baseURL,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
html2pdfConverter: html2pdfConverter,
|
html2pdfConverter: html2pdfConverter,
|
||||||
fileManager: fileManagerService,
|
fileManager: fileManagerService,
|
||||||
@@ -120,7 +120,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
|
|||||||
proboSvc: s.proboSvc,
|
proboSvc: s.proboSvc,
|
||||||
encryptionKey: s.encryptionKey,
|
encryptionKey: s.encryptionKey,
|
||||||
tokenSecret: s.tokenSecret,
|
tokenSecret: s.tokenSecret,
|
||||||
hostname: s.hostname,
|
baseURL: s.baseURL,
|
||||||
auth: s.auth,
|
auth: s.auth,
|
||||||
html2pdfConverter: s.html2pdfConverter,
|
html2pdfConverter: s.html2pdfConverter,
|
||||||
fileManager: s.fileManager,
|
fileManager: s.fileManager,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/getprobo/probo/pkg/baseurl"
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/slack"
|
"github.com/getprobo/probo/pkg/slack"
|
||||||
@@ -409,6 +410,11 @@ func (s *SlackMessageService) buildAccessRequestMessage(
|
|||||||
fileIDs = append(fileIDs, file.ID)
|
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 {
|
templateData := struct {
|
||||||
RequesterName string
|
RequesterName string
|
||||||
RequesterEmail string
|
RequesterEmail string
|
||||||
@@ -425,7 +431,7 @@ func (s *SlackMessageService) buildAccessRequestMessage(
|
|||||||
RequesterName: requesterName,
|
RequesterName: requesterName,
|
||||||
RequesterEmail: requesterEmail,
|
RequesterEmail: requesterEmail,
|
||||||
OrganizationID: organizationID.String(),
|
OrganizationID: organizationID.String(),
|
||||||
Domain: s.svc.hostname,
|
Domain: base.Host(),
|
||||||
SlackMessageID: slackMessageID.String(),
|
SlackMessageID: slackMessageID.String(),
|
||||||
DocumentIDs: documentIDs,
|
DocumentIDs: documentIDs,
|
||||||
ReportIDs: reportIDs,
|
ReportIDs: reportIDs,
|
||||||
|
|||||||
@@ -470,7 +470,13 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
|
|||||||
return fmt.Errorf("cannot load organization: %w", err)
|
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"
|
path := "/trust/" + trustCenter.Slug + "/access"
|
||||||
|
|
||||||
if organization.CustomDomainID != nil {
|
if organization.CustomDomainID != nil {
|
||||||
@@ -484,11 +490,12 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
hostname = customDomain.Domain
|
hostname = customDomain.Domain
|
||||||
|
scheme = "https"
|
||||||
path = "/access"
|
path = "/access"
|
||||||
}
|
}
|
||||||
|
|
||||||
accessURL := url.URL{
|
accessURL := url.URL{
|
||||||
Scheme: "https",
|
Scheme: scheme,
|
||||||
Host: hostname,
|
Host: hostname,
|
||||||
Path: path,
|
Path: path,
|
||||||
RawQuery: url.Values{
|
RawQuery: url.Values{
|
||||||
@@ -517,7 +524,7 @@ func (s *TrustCenterAccessService) sendTrustCenterAccessEmail(
|
|||||||
accessURL string,
|
accessURL string,
|
||||||
) error {
|
) error {
|
||||||
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
|
subject, textBody, htmlBody, err := emails.RenderTrustCenterAccess(
|
||||||
s.svc.hostname,
|
s.svc.baseURL,
|
||||||
name,
|
name,
|
||||||
companyName,
|
companyName,
|
||||||
accessURL,
|
accessURL,
|
||||||
|
|||||||
Reference in New Issue
Block a user