Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first basic i18n support for webpage #70

Merged
merged 5 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -155,16 +162,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 @@ -193,7 +194,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>
Loading