refactor(ext-oidc): replace auth modules with security extensions #17

Merged
Relism merged 12 commits from feature/ext-auth/split-oidc-into-auth-core into master 2026-09-16 16:00:15 +00:00
7 changed files with 128 additions and 11 deletions
Showing only changes of commit 4feabc45d5 - Show all commits
@@ -15,6 +15,23 @@ import java.util.function.Function;
public abstract class BaseViewExtension<TTarget> implements FlashExtension {
private final List<GlobalValue> globals = new ArrayList<>();
protected BaseViewExtension() {}
/**
* Seeds this instance with globals carried over from a prior one. Subclasses whose
* fluent settings methods return a new instance (e.g. {@code JteExtension.templateRoot(...)})
* must route through this constructor — otherwise {@link #addGlobal} calls made before
* such a method silently vanish, since the new instance would start with an empty list.
*/
protected BaseViewExtension(List<GlobalValue> seedGlobals) {
globals.addAll(seedGlobals);
}
/** Snapshot of globals registered so far — for subclasses to carry over into a new instance. */
protected final List<GlobalValue> globals() {
return List.copyOf(globals);
}
public BaseViewExtension<TTarget> addGlobal(String key, Function<Request, Object> resolver) {
String k = Objects.requireNonNull(key, "global key must not be null").trim();
if (k.isEmpty()) {
@@ -34,7 +51,13 @@ public abstract class BaseViewExtension<TTarget> implements FlashExtension {
@Override
public void configure(FlashRegistrar<?> app, FlashContext ctx) {
ViewRuntimeBridge<TTarget> runtime = createRuntime(List.copyOf(globals));
ctx.provide(ViewRuntimeBridge.class, runtime);
try {
ctx.provide(ViewRuntimeBridge.class, runtime);
} catch (IllegalStateException e) {
throw new IllegalStateException("Another flash-ext-view implementation is already installed in "
+ "this FlashApp. Only one view engine (e.g. ThymeleafExtension or JteExtension, not both) "
+ "can be active per app — install a single implementation.", e);
}
ctx.addAnnotationProcessor(handlerClass -> {
validateHandlerClass(handlerClass);
return List.of();
@@ -0,0 +1,40 @@
package dev.relism.flash.ext.view.core;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.models.RequestHandler;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class BaseViewExtensionTest {
private static final class FakeViewExtension extends BaseViewExtension<Object> {
@Override
protected ViewRuntimeBridge<Object> createRuntime(List<GlobalValue> globals) {
return new ViewRuntimeBridge<>() {
@Override public Object resolve(Class<?> handlerClass) { return null; }
@Override public RenderedView render(BaseViewHandler<Object> handler, Object target,
dev.relism.flash.models.Request req,
dev.relism.flash.models.Response res) { return null; }
};
}
@Override
protected void validateHandlerClass(Class<? extends RequestHandler> handlerClass) {}
}
@Test
void configure_twoViewEngines_failsWithClearMessage() {
FlashContext ctx = new FlashContext();
new FakeViewExtension().configure(null, ctx);
IllegalStateException ex = assertThrows(IllegalStateException.class,
() -> new FakeViewExtension().configure(null, ctx));
assertTrue(ex.getMessage().contains("Only one view engine"),
"expected a clear cross-engine message, got: " + ex.getMessage());
}
}
@@ -8,7 +8,7 @@ This module keeps jte semantics front and center:
- `JteHandler`
- `@Template`
- `ViewModel` from `flash-ext-view-core`
- `global.*` reserved namespace
- globals merged flat into the model, one typed `@param` per global
The extension mirrors `gg.jte.ContentType` into Flash HTTP content type:
@@ -9,7 +9,7 @@
2. **Request-time**
- Handler builds local `ViewModel`.
- Runtime injects globals under reserved `global` namespace and merges local model.
- Runtime merges globals and local model into one flat parameter map (fails fast on key collision).
- jte renders template into `StringOutput`.
3. **Static assets (optional, enabled by default)**
@@ -28,6 +28,21 @@ Register globals in extension setup:
new JteExtension().addGlobal("appName", req -> "Flash")
```
Globals are available under `global` namespace in templates.
Globals are merged **flat** into the model — same level as local `ViewModel` keys, not
under a `global.*` namespace. jte templates are statically typed, so each template declares
one `@param` per global it actually uses, named exactly like the global key:
`global` is reserved and cannot be used as local model key.
```jte
@param String appName
<span>${appName}</span>
```
A template that doesn't declare `appName` simply never sees it — no need to declare every
global on every page, only the ones a given template actually uses.
A local `ViewModel` key that collides with a registered global name fails fast with
`IllegalStateException` at render time, so a typo can't silently shadow a global.
Every registered global is still resolved on **every** render regardless of whether the
target template declares it, so keep resolvers cheap (no blocking I/O, no heavy allocation).
@@ -29,31 +29,32 @@ public final class JteExtension extends BaseViewExtension<JteTarget> {
this.settings = builder.build();
}
private JteExtension(JteSettings settings) {
private JteExtension(JteSettings settings, List<GlobalValue> seedGlobals) {
super(seedGlobals);
this.settings = settings;
}
public JteExtension templateRoot(String templateRoot) {
return new JteExtension(settings.toBuilder().templateRoot(templateRoot).build());
return new JteExtension(settings.toBuilder().templateRoot(templateRoot).build(), globals());
}
public JteExtension serveStatics(boolean serveStatics) {
return new JteExtension(settings.toBuilder().serveStatics(serveStatics).build());
return new JteExtension(settings.toBuilder().serveStatics(serveStatics).build(), globals());
}
public JteExtension staticPrefix(String staticPrefix) {
return new JteExtension(settings.toBuilder().staticPrefix(staticPrefix).build());
return new JteExtension(settings.toBuilder().staticPrefix(staticPrefix).build(), globals());
}
public JteExtension withStaticCors() {
return new JteExtension(settings.toBuilder().enableStaticCors(true).build());
return new JteExtension(settings.toBuilder().enableStaticCors(true).build(), globals());
}
public JteExtension staticCors(Consumer<JteSettings.Builder> corsConfig) {
JteSettings.Builder builder = settings.toBuilder();
builder.enableStaticCors(true);
corsConfig.accept(builder);
return new JteExtension(builder.build());
return new JteExtension(builder.build(), globals());
}
@Override
@@ -1,6 +1,11 @@
package dev.relism.flash.ext.view.jte;
import dev.relism.flash.ext.view.core.RenderedView;
import dev.relism.flash.ext.view.core.ViewModel;
import dev.relism.flash.ext.view.core.ViewRuntimeBridge;
import dev.relism.flash.ext.view.jte.model.HomePage;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
@@ -89,6 +94,39 @@ class JteExtensionTest {
assertEquals("https://cdn.example.com", settings.staticCorsAllowOrigin());
}
@Test
void addGlobal_survives_subsequent_settings_builder_calls() throws Exception {
// addGlobal() before a chained settings method (templateRoot/serveStatics/...) used to
// be silently dropped: those methods return a *new* JteExtension instance, and the
// globals list lived on the instance, not the settings being carried forward.
JteExtension ext = new JteExtension(cfg -> cfg.developmentMode(true))
.addGlobal("appName", req -> "FlashLab")
.templateRoot("templates")
.serveStatics(false);
TestRegistrar app = new TestRegistrar();
FlashContext ctx = new FlashContext();
ext.configure(app, ctx);
ctx.complete();
@SuppressWarnings("unchecked")
ViewRuntimeBridge<JteTarget> runtime = (ViewRuntimeBridge<JteTarget>) ctx.require(ViewRuntimeBridge.class);
JteHandler handler = new JteHandler() {
@Override
public ViewModel render(Request req) {
return ViewModel.empty()
.with("page", new HomePage("Flash + jte", "elorc"))
.with("build", "dev");
}
};
JteTarget target = new JteTarget("pages/home.jte", ContentType.TEXT_HTML);
RenderedView out = runtime.render(handler, target, request("/", null, null, null),
new Response(200, ContentType.JSON));
assertTrue(out.body().contains("FlashLab"), "global registered before templateRoot() must survive");
}
@Test
void routes_register_static_wildcard_when_enabled() {
JteExtension ext = new JteExtension(cfg -> cfg.staticPrefix("/assets"));