ConfirmationLinkBuilder.java

/*
 * Copyright (c) 2007-2026 MetaSolutions AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.entrystore.rest.springboot.service.auth;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Strings;
import org.entrystore.impl.RepositoryManagerImpl;
import org.entrystore.rest.springboot.configuration.ConfirmationUrlProperties;
import org.springframework.stereotype.Component;

import java.net.URI;

import static org.entrystore.rest.springboot.configuration.ConfirmationUrlProperties.TOKEN_PLACEHOLDER;

/**
 * Builds the link in a password-reset or sign-up confirmation email: the configured
 * {@code entrystore.auth.confirmation.url.*} template with the token substituted, else the backend's own
 * confirmation page. The link is the same in legacy and credential-confirmation mode.
 *
 * <p>A relative template is resolved against the repository base URL, taken as ending with {@code /}: a leading
 * {@code /} resolves against the host, a template without one is appended to the base path. A template that does
 * not resolve to an http(s) URL with a host fails startup.
 */
@Slf4j
@Component
public class ConfirmationLinkBuilder {

	private static final String PREFIX = "entrystore.auth.confirmation.url.";

	private final String passwordResetTemplate;
	private final String signupTemplate;

	public ConfirmationLinkBuilder(ConfirmationUrlProperties urls, RepositoryManagerImpl repositoryManager) {
		String repositoryUrl = repositoryManager.getRepositoryURL().toExternalForm();
		this.passwordResetTemplate = urls.pwreset() != null
				? resolve("pwreset", urls.pwreset(), repositoryUrl)
				: repositoryUrl + "auth/pwreset?confirm=" + TOKEN_PLACEHOLDER;
		this.signupTemplate = urls.signup() != null
				? resolve("signup", urls.signup(), repositoryUrl)
				: repositoryUrl + "auth/signup?confirm=" + TOKEN_PLACEHOLDER;
		log.info("Password-reset confirmation emails link to {}", passwordResetTemplate);
		log.info("Sign-up confirmation emails link to {}", signupTemplate);
	}

	public String passwordResetLink(String token) {
		return passwordResetTemplate.replace(TOKEN_PLACEHOLDER, token);
	}

	public String signupLink(String token) {
		return signupTemplate.replace(TOKEN_PLACEHOLDER, token);
	}

	private static String resolve(String key, String template, String repositoryUrl) {
		// The placeholder is URI-safe, so it survives resolution unchanged.
		URI resolved = URI.create(Strings.CS.appendIfMissing(repositoryUrl, "/")).resolve(URI.create(template));
		if (!Strings.CI.equalsAny(resolved.getScheme(), "http", "https") || resolved.getHost() == null) {
			throw new IllegalArgumentException(PREFIX + key + " must resolve to an http(s) URL with a host: "
					+ template);
		}
		return resolved.toString();
	}
}