Skip to content

Commit

Permalink
first basic i18n support for webpage (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuellvo authored Jul 20, 2023
1 parent 0100a87 commit 1e6f74f
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 19 deletions.
10 changes: 10 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@
<name>moth-server</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import edu.sjsu.moth.server.util.Util.TTLHashMap;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -19,9 +20,10 @@
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.security.Principal;
import java.security.SecureRandom;
import java.time.LocalDateTime;
Expand All @@ -33,6 +35,8 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import static edu.sjsu.moth.server.controller.i18nController.getExceptionMessage;

/**
* This code handles first contact and oauth outh with the client.
* The flow seems to be:
Expand Down Expand Up @@ -76,6 +80,9 @@ public class AppController {
TTLHashMap<String, AppRegistrationEntry> registrations = new TTLHashMap<>(10, TimeUnit.MINUTES);
@Autowired
TokenRepository tokenRepository;
// required for i18n
@Autowired
private SpringTemplateEngine templateEngine;

/**
* base64 URL encode a nonce of byteCount random bytes.
Expand Down Expand Up @@ -162,16 +169,10 @@ private CredentialAccount convertAccount2CredentialAccount(Account a) {
}

@GetMapping("/oauth/authorize")
String getOauthAuthorize(@RequestParam String client_id, @RequestParam String redirect_uri,
@RequestParam(required = false, defaultValue = "") String error) throws IOException {
try (var is = AppController.class.getResourceAsStream("/static/oauth/authorize.html")) {
var authorizePage = new String(is.readAllBytes());
authorizePage = authorizePage.replace("client_id_value", client_id);
authorizePage = authorizePage.replace("redirect_uri_value", redirect_uri);
authorizePage = authorizePage.replace("authorize_error", error);
//noinspection ReassignedVariable
return authorizePage;
}
public String getOauthAuthorize() {
// resolves locale to user locale; resolves the locale based on the "Accept-Language" header in the
// request packet. resolved via the WebFilterChain.
return templateEngine.process("authorize", new Context(LocaleContextHolder.getLocale()));
}

@GetMapping("/oauth/login")
Expand Down Expand Up @@ -200,7 +201,8 @@ Mono<ResponseEntity<Object>> postOauthToken(@RequestBody TokenRequest req) {
scopes = req.scope;
} else {
if (!registration.registration.client_secret.equals(req.client_secret)) {
throw new RuntimeException("bad client_secret");
throw new RuntimeException(
getExceptionMessage("clientSecretException", LocaleContextHolder.getLocale()));
}
scopes = registration.scopes;
name = registration.registration.name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package edu.sjsu.moth.server.controller;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

import java.util.Locale;
import java.util.ResourceBundle;

@Configuration
public class i18nController {
//from baeldung https://www.baeldung.com/java-localize-exception-messages
public static String getExceptionMessage(String key, Locale locale) {
return ResourceBundle.getBundle("messages", locale).getString(key);
}

@Bean
// message source is responsible for retrieving messages based on current locale. it loads different message sets
// and
// applies as necessary. message sets r based off of 'basename'. (e.g, spanish is messages_es now since basename is
// messages
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}

// this method searches for the template to dynamically load based on prefix (filepath) and suffix. required for
// template engine.
public ClassLoaderTemplateResolver templateResolver() {
ClassLoaderTemplateResolver templateSettings = new ClassLoaderTemplateResolver();
templateSettings.setPrefix(
"static/oauth/"); // classpath for the template, if/when we need more pages in the future we
//should probably rename this folder for readability
templateSettings.setSuffix(".html"); // suffix for templates so it knows what to load
templateSettings.setTemplateMode("HTML"); // can switch between HTML, XML, and text
templateSettings.setCharacterEncoding("UTF-8"); //only involves reading, doesn't overlap messageSource encoding
return templateSettings;
}

@Bean
// CORE component of Thymeleaf -- it takes and parses a template, requires context for various details e.g locale,
// and renders page, outputting HTML
// all current and future autowires will refer to this one
public SpringTemplateEngine springTemplateEngine() {
SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
springTemplateEngine.setMessageSource(messageSource()); // message sets, defined above
springTemplateEngine.setTemplateResolver(templateResolver());
return springTemplateEngine;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.Locale;
import java.util.Map;
import java.util.Set;

Expand All @@ -32,6 +39,24 @@ public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http.csrf().disable();
http.oauth2ResourceServer().opaqueToken().introspector(this::introspect);
http.addFilterBefore(localeChangeFilter(), SecurityWebFiltersOrder.FIRST);
return http.build();
}

public LocaleChangeFilter localeChangeFilter() {
return new LocaleChangeFilter();
}

public static class LocaleChangeFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String acceptLanguage = exchange.getRequest().getHeaders().getFirst(HttpHeaders.ACCEPT_LANGUAGE);
// to improve this code, i would say you could technically process the data grabbed above by "Q" value to
// switch to the most 'preferred' value by the User.
Locale resolvedLocale = (acceptLanguage != null) ? Locale.forLanguageTag(
acceptLanguage) : Locale.getDefault();
LocaleContextHolder.setLocale(resolvedLocale);
return chain.filter(exchange);
}
}
}
3 changes: 2 additions & 1 deletion server/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
spring.main.web-application-type=REACTIVE
# NONE, SERVLET
# NONE, SERVLET
spring.thymeleaf.prefix=classpath:/oauth/
5 changes: 5 additions & 0 deletions server/src/main/resources/messages.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
authorizeError=Authorize_Error
email=Email
password=Password
signIn=Sign In
clientSecretException=Bad Client Secret
5 changes: 5 additions & 0 deletions server/src/main/resources/messages_es.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
authorizeError=Autorizar_Error
email=Email
password=Contrasena
signIn=Acceso
clientSecretException=Secreto de Cliente Incorrecto
15 changes: 9 additions & 6 deletions server/src/main/resources/static/oauth/authorize.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<link rel="icon" type="image/png" sizes="32x32" href="/moth/favicon.png">
<meta charset="UTF-8">
Expand Down Expand Up @@ -73,15 +73,18 @@
<div class="spin">
<img alt="Moth Logo" src="/moth/cyber-moth.png">
</div>
<h2>authorize_error</h2>
<h2 th:text="#{authorizeError}">authError</h2>
<form action="/oauth/login">
Email<br/>
<input name="user"/><br/>
Password<br/>
<span th:text="#{email}">Email</span>
<br/>
<input name="user"/>
<br/>
<span th:text="#{password}">Password</span>
<br/>
<input name="password" type="password"/><br/>
<input name="client_id" type="hidden" value="client_id_value"/>
<input name="redirect_uri" type="hidden" value="redirect_uri_value"/>
<input type="submit" value="Sign In"/>
<input type="submit" value="#{signIn}" th:value="#{signIn}"/>
</form>
</body>
</html>

0 comments on commit 1e6f74f

Please sign in to comment.