mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Cleanup of SaaS code (#6669)
# Description of Changes De-AI comments and fix ridiculously indented code
This commit is contained in:
+23
-34
@@ -24,6 +24,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.OAuth2ProtectedResourceMetadata;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
@@ -201,40 +202,9 @@ public class McpSecurityConfig {
|
||||
.protectedResourceMetadata(
|
||||
prm ->
|
||||
prm.protectedResourceMetadataCustomizer(
|
||||
builder -> {
|
||||
if (!auth.getResourceId()
|
||||
.isBlank()) {
|
||||
builder.resource(
|
||||
auth
|
||||
.getResourceId());
|
||||
}
|
||||
if (!auth.getIssuerUri()
|
||||
.isBlank()) {
|
||||
builder.authorizationServer(
|
||||
auth
|
||||
.getIssuerUri());
|
||||
}
|
||||
// Only advertise the granular
|
||||
// tool scopes when we actually
|
||||
// enforce them. When scopes are
|
||||
// disabled (e.g. the IdP only
|
||||
// mints coarse tokens, like
|
||||
// Supabase), advertising scopes
|
||||
// the authorization server
|
||||
// can't
|
||||
// issue makes spec-compliant
|
||||
// clients request them and get
|
||||
// rejected with
|
||||
// invalid_request.
|
||||
if (applicationProperties
|
||||
.getMcp()
|
||||
.isScopesEnabled()) {
|
||||
builder.scope(
|
||||
"mcp.tools.read");
|
||||
builder.scope(
|
||||
"mcp.tools.write");
|
||||
}
|
||||
}))
|
||||
builder ->
|
||||
buildResourceMetadata(
|
||||
builder, auth)))
|
||||
.jwt(
|
||||
jwt ->
|
||||
jwt.decoder(mcpJwtDecoder)
|
||||
@@ -243,6 +213,25 @@ public class McpSecurityConfig {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/** Populate the RFC 9728 protected-resource metadata document from the configured auth. */
|
||||
private void buildResourceMetadata(
|
||||
OAuth2ProtectedResourceMetadata.Builder builder, ApplicationProperties.Mcp.Auth auth) {
|
||||
if (!auth.getResourceId().isBlank()) {
|
||||
builder.resource(auth.getResourceId());
|
||||
}
|
||||
if (!auth.getIssuerUri().isBlank()) {
|
||||
builder.authorizationServer(auth.getIssuerUri());
|
||||
}
|
||||
// Only advertise the granular tool scopes when we actually enforce them. When scopes are
|
||||
// disabled (e.g. the IdP only mints coarse tokens, like Supabase), advertising scopes the
|
||||
// authorization server can't issue makes spec-compliant clients request them and get
|
||||
// rejected with invalid_request.
|
||||
if (applicationProperties.getMcp().isScopesEnabled()) {
|
||||
builder.scope("mcp.tools.read");
|
||||
builder.scope("mcp.tools.write");
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoder mcpJwtDecoder() {
|
||||
ApplicationProperties.Mcp.Auth auth = applicationProperties.getMcp().getAuth();
|
||||
|
||||
@@ -303,9 +303,9 @@ public class PaygWalletController {
|
||||
} else {
|
||||
long capMinor = CapMoneyUnits.usdToCents(req.capUsd());
|
||||
policy.setCapSourceMoney(capMinor);
|
||||
// Derived document allowance (design §10: store both the money intent and the unit
|
||||
// translation). The live snapshot recomputes from cap_source_money + current rate;
|
||||
// this stored value is the enforcement fallback when the rate is unreachable.
|
||||
// Derived document allowance: store both the money intent and the unit translation.
|
||||
// The live snapshot recomputes from cap_source_money + current rate; this stored value
|
||||
// is the enforcement fallback when the rate is unreachable.
|
||||
TeamBillingContext billing = billingService.forTeam(teamId);
|
||||
Optional<Long> docCap = billingService.docCapForMoney(billing, capMinor);
|
||||
if (docCap.isPresent()) {
|
||||
|
||||
@@ -82,8 +82,7 @@ public record WalletSnapshotResponse(
|
||||
|
||||
/**
|
||||
* One row of the team-members table on the leader's Plan page — display-only per-member usage.
|
||||
* (Per-member sub-caps aren't enforced yet; see the follow-ups note. When they ship, a cap
|
||||
* field returns here.)
|
||||
* (Per-member sub-caps aren't enforced yet. When they ship, a cap field returns here.)
|
||||
*/
|
||||
public record MemberRow(String userId, String name, String email, int spendUnits) {}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import stirling.software.saas.payg.model.FeatureSet;
|
||||
* effect, and the corresponding enabled {@link FeatureGate}s. No DB access, no caches — the caller
|
||||
* (entitlement service) supplies the inputs.
|
||||
*
|
||||
* <p>State transitions (matching {@code notes/PAYG_DESIGN.md} §3.6):
|
||||
* <p>State transitions:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code capUnits == null} → {@code FULL} / {@link FeatureSet#FULL} unconditionally.
|
||||
@@ -78,10 +78,7 @@ public final class CapEvaluator {
|
||||
return full();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default enabled gates for a given feature set. Kept in sync with the design doc §3.7 mapping
|
||||
* table.
|
||||
*/
|
||||
/** Default enabled gates for a given feature set. */
|
||||
public static List<FeatureGate> gatesFor(FeatureSet set) {
|
||||
if (set == null) {
|
||||
return List.of();
|
||||
|
||||
+9
-9
@@ -20,17 +20,17 @@ import lombok.extern.slf4j.Slf4j;
|
||||
/**
|
||||
* Read-only accessor for the Stripe Sync Engine schema ({@code stripe.*}). Gives the PAYG layer the
|
||||
* team's real billing window and the per-document rate of the Price its subscription bills against
|
||||
* — both live in Stripe, mirrored into Postgres by the sync engine, and are NOT duplicated in
|
||||
* {@code stirling_pdf} (design §10: money lives in Stripe).
|
||||
* - both live in Stripe, mirrored into Postgres by the sync engine, and are NOT duplicated in
|
||||
* {@code stirling_pdf} (money lives in Stripe).
|
||||
*
|
||||
* <p>PAYG prices are plain {@code per_unit} metered prices, so {@code stripe.prices.unit_amount}
|
||||
* carries the rate directly. The free grant is deliberately NOT in Stripe — it's the one-time
|
||||
* carries the rate directly. The free grant is deliberately NOT in Stripe - it's the one-time
|
||||
* {@code pricing_policy.free_tier_units} pool, applied app-side (free units are never metered),
|
||||
* because un-subscribed teams get the same grant and have no Stripe Price at all.
|
||||
*
|
||||
* <p>Defensive by construction: the {@code stripe} schema only exists where the sync engine has run
|
||||
* (dev + prod Supabase, not unit-test H2). Any {@link DataAccessException} — missing schema,
|
||||
* missing row, connectivity blip — degrades to {@link Optional#empty()} with a WARN so callers fall
|
||||
* (dev + prod Supabase, not unit-test H2). Any {@link DataAccessException} - missing schema,
|
||||
* missing row, connectivity blip - degrades to {@link Optional#empty()} with a WARN so callers fall
|
||||
* back to calendar-month windows rather than 500ing the wallet endpoint.
|
||||
*/
|
||||
@Slf4j
|
||||
@@ -60,13 +60,13 @@ public class StripeSubscriptionDao {
|
||||
BigDecimal perDocMinor) {}
|
||||
|
||||
/**
|
||||
* The per-document rate of a Price looked up directly (not via a subscription) — used to price
|
||||
* The per-document rate of a Price looked up directly (not via a subscription) - used to price
|
||||
* the cap estimate for un-subscribed teams, whose default policy points at Stripe Prices that
|
||||
* carry the same {@code unit_amount} they'd be billed at on subscribing.
|
||||
*
|
||||
* @param priceId the resolved Stripe Price id
|
||||
* @param currency lower-case ISO 4217 of that Price
|
||||
* @param perDocMinor per-document rate in minor units (may be fractional); never null — a row
|
||||
* @param perDocMinor per-document rate in minor units (may be fractional); never null - a row
|
||||
* with no usable amount is filtered out rather than returned with a null rate
|
||||
*/
|
||||
public record PriceRate(String priceId, String currency, BigDecimal perDocMinor) {}
|
||||
@@ -130,12 +130,12 @@ public class StripeSubscriptionDao {
|
||||
|
||||
/**
|
||||
* Per-document rate of the active {@code stripe.prices} row with the given {@code lookupKey} in
|
||||
* {@code currency} — the elegant mirror of {@link #findBilling}, reading the same synced table
|
||||
* {@code currency} - the elegant mirror of {@link #findBilling}, reading the same synced table
|
||||
* by Stripe Price {@code lookup_key} instead of via a subscription. The PAYG layer uses this to
|
||||
* price the cap estimate for an un-subscribed team: there's no subscription to read a rate off,
|
||||
* but the PAYG Price (lookup key {@code plan:processor}) carries the very rate they'd be billed
|
||||
* at. We resolve by lookup_key rather than the default policy's price ids because those aren't
|
||||
* seeded — the lookup key is the stable, env-agnostic handle (same one the price-lookup edge
|
||||
* seeded - the lookup key is the stable, env-agnostic handle (same one the price-lookup edge
|
||||
* function uses).
|
||||
*
|
||||
* <p>Empty when the {@code stripe} schema is absent (H2 unit tests), no active matching row
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
-- restored on a first-step refund). Because the counter is authoritative, the wallet_ledger is
|
||||
-- no longer the source of truth for the grant and its old rows can be pruned after a retention
|
||||
-- window (separate future job).
|
||||
--
|
||||
-- See notes/payg-lifetime-free-grant-plan-2026-06-11.html.
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------
|
||||
-- 1. Rename the policy column — it is no longer "per cycle", it's the one-time grant size.
|
||||
|
||||
Reference in New Issue
Block a user