From c471503ede9aad91193ff6f93966196e6aff15d6 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 4 Jan 2023 03:38:19 +0300 Subject: OAuth authentication for Mastodon and ActivityPub C2S --- src/main/java/com/juick/config/SecurityConfig.java | 104 ++++++++++++++---- .../juick/service/security/annotation/Visitor.java | 29 ----- .../java/com/juick/www/SiteAttributesHandler.java | 25 +++++ src/main/java/com/juick/www/ad/SapeService.java | 3 +- src/main/java/com/juick/www/api/Mastodon.java | 118 +++++++++++++++++++++ src/main/java/com/juick/www/api/Messages.java | 18 ++-- src/main/java/com/juick/www/api/Notifications.java | 88 +++++---------- src/main/java/com/juick/www/api/PM.java | 12 +-- src/main/java/com/juick/www/api/Post.java | 21 ++-- src/main/java/com/juick/www/api/Service.java | 12 +-- src/main/java/com/juick/www/api/Tags.java | 8 +- src/main/java/com/juick/www/api/Users.java | 27 ++--- .../java/com/juick/www/api/activity/Profile.java | 13 +-- src/main/java/com/juick/www/controllers/Help.java | 4 +- .../java/com/juick/www/controllers/Settings.java | 8 +- .../java/com/juick/www/controllers/SignUp.java | 6 +- src/main/java/com/juick/www/controllers/Site.java | 32 +++--- .../com/juick/www/controllers/SocialLogin.java | 11 +- src/main/java/com/juick/www/rss/Feeds.java | 10 +- src/main/java/com/juick/www/rss/MessagesView.java | 62 +++++------ .../db/migration/V1.29__oauth_clients.sql | 15 +++ .../java/com/juick/server/tests/ServerTests.java | 17 +++ 22 files changed, 383 insertions(+), 260 deletions(-) delete mode 100644 src/main/java/com/juick/service/security/annotation/Visitor.java create mode 100644 src/main/java/com/juick/www/api/Mastodon.java create mode 100644 src/main/resources/db/migration/V1.29__oauth_clients.sql (limited to 'src') diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java index d2030a62..d60abe00 100644 --- a/src/main/java/com/juick/config/SecurityConfig.java +++ b/src/main/java/com/juick/config/SecurityConfig.java @@ -25,20 +25,37 @@ import com.juick.service.security.HTTPSignatureAuthenticationFilter; import com.juick.service.security.HashParamAuthenticationFilter; import com.juick.service.security.JuickUserDetailsService; import com.juick.service.security.entities.JuickUser; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.*; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @@ -48,8 +65,12 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import javax.inject.Inject; +import java.io.IOException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.Collections; +import java.util.UUID; /** * Created by aalexeev on 11/21/16. @@ -61,7 +82,8 @@ public class SecurityConfig { private UserService userService; @Inject private KeystoreManager keystoreManager; - + @Inject + private JdbcTemplate jdbcTemplate; private static final String COOKIE_NAME = "juick-remember-me"; @Bean UserDetailsService userDetailsService() { @@ -77,6 +99,7 @@ public class SecurityConfig { configuration.setAllowedHeaders(Collections.singletonList("*")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/oauth/**", configuration); source.registerCorsConfiguration("/api/**", configuration); source.registerCorsConfiguration("/u/**", configuration); source.registerCorsConfiguration("/n/**", configuration); @@ -124,14 +147,60 @@ public class SecurityConfig { services.setUseSecureCookie(false); // TODO set true if https is supports return services; } + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) + throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .authorizationServerSettings(AuthorizationServerSettings.builder() + .authorizationEndpoint("/oauth/authorize") + .tokenEndpoint("/oauth/token") + .build()) + .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 + http + // Redirect to the login page when not authenticated from the + // authorization endpoint + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) + ) + // Accept access tokens for User Info and/or Client Registration + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + + return http.formLogin(Customizer.withDefaults()).build(); + } + @Bean + public RegisteredClientRepository registeredClientRepository() { + return new JdbcRegisteredClientRepository(jdbcTemplate); + } + + @Bean + public JWKSource jwkSource() { + RSAPublicKey publicKey = (RSAPublicKey) keystoreManager.getPublicKey(); + RSAPrivateKey privateKey = (RSAPrivateKey) keystoreManager.getPrivateKey(); + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } @Bean + @Order(2) SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**") .addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class) .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) - .addFilterBefore(bearerTokenAuthenticationFilter(), BasicAuthenticationFilter.class) .authorizeHttpRequests(requests -> requests .requestMatchers(HttpMethod.OPTIONS).permitAll() .requestMatchers("/api/", "/api/messages", "/api/avatar", @@ -142,15 +211,16 @@ public class SecurityConfig { "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/_applelogin", "/api/signup", "/api/inbox", "/api/events", "/api/u/", - "/api/info/**", - "/api/nodeinfo/2.0") + "/api/info/**", "/api/v1/apps", "/api/v1/instance", + "/api/nodeinfo/2.0", "/oauth/**") .permitAll() - .anyRequest().hasRole("USER")) + .anyRequest().hasAnyAuthority("SCOPE_write", "ROLE_USER")) .anonymous(anonymous -> anonymous.principal(JuickUser.ANONYMOUS_USER) .authorities(JuickUser.ANONYMOUS_AUTHORITY)) .httpBasic(httpBasic -> httpBasic .authenticationEntryPoint(apiAuthenticationEntryPoint())) .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) .sessionManagement(sessionManagement -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(exceptionHandling -> exceptionHandling @@ -159,14 +229,6 @@ public class SecurityConfig { .headers().defaultsDisabled().cacheControl(); return http.build(); } - - @Bean - AuthenticationSuccessHandler successHandler() { - SimpleUrlAuthenticationSuccessHandler handler = new SimpleUrlAuthenticationSuccessHandler(); - handler.setUseReferer(true); - return handler; - } - @Bean SecurityFilterChain h2ConsoFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/h2-console/**") @@ -182,7 +244,12 @@ public class SecurityConfig { .headers().defaultsDisabled().cacheControl(); return http.build(); } - + @Bean + AuthenticationSuccessHandler successHandler() { + var handler = new SavedRequestAwareAuthenticationSuccessHandler(); + handler.setUseReferer(true); + return handler; + } @Bean SecurityFilterChain wwwChain(HttpSecurity http) throws Exception { http.addFilterBefore(wwwAuthenticationFilter(), BasicAuthenticationFilter.class) @@ -197,9 +264,6 @@ public class SecurityConfig { .authorities(JuickUser.ANONYMOUS_AUTHORITY)) .cors(cors -> cors .configurationSource(corsConfigurationSource())) - .sessionManagement( - sessionManagement -> sessionManagement - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .invalidateHttpSession(true) diff --git a/src/main/java/com/juick/service/security/annotation/Visitor.java b/src/main/java/com/juick/service/security/annotation/Visitor.java deleted file mode 100644 index 2bad7e4b..00000000 --- a/src/main/java/com/juick/service/security/annotation/Visitor.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2008-2020, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.service.security.annotation; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; - -import java.lang.annotation.*; - -@Target({ ElementType.PARAMETER, ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@AuthenticationPrincipal(errorOnInvalidType = true, expression = "user") -public @interface Visitor { -} diff --git a/src/main/java/com/juick/www/SiteAttributesHandler.java b/src/main/java/com/juick/www/SiteAttributesHandler.java index e06a2070..4ead9d1e 100644 --- a/src/main/java/com/juick/www/SiteAttributesHandler.java +++ b/src/main/java/com/juick/www/SiteAttributesHandler.java @@ -17,6 +17,14 @@ package com.juick.www; +import com.juick.model.AnonymousUser; +import com.juick.model.User; +import com.juick.service.UserService; +import com.juick.service.security.entities.JuickUser; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; @@ -24,8 +32,12 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import jakarta.servlet.http.HttpServletResponse; +import javax.inject.Inject; + @ControllerAdvice public class SiteAttributesHandler { + @Inject + private UserService userService; @ModelAttribute public void setVaryResponseHeader(HttpServletResponse response) { response.setHeader("Vary", "Accept-Language"); @@ -34,4 +46,17 @@ public class SiteAttributesHandler { public void setReturnPathAttribute(Model model) { model.addAttribute("retpath", ServletUriComponentsBuilder.fromCurrentRequestUri().toUriString()); } + @ModelAttribute + public User visitor(Model model, @AuthenticationPrincipal Object principal) { + if (principal != null) { + if (principal instanceof JuickUser) { + return ((JuickUser) principal).getUser(); + } + if (principal instanceof Jwt) { + var uname = (String) ((Jwt) principal).getClaims().get("sub"); + return userService.getUserByName(uname); + } + } + return AnonymousUser.INSTANCE; + } } diff --git a/src/main/java/com/juick/www/ad/SapeService.java b/src/main/java/com/juick/www/ad/SapeService.java index 3c35f320..7b398806 100644 --- a/src/main/java/com/juick/www/ad/SapeService.java +++ b/src/main/java/com/juick/www/ad/SapeService.java @@ -18,7 +18,6 @@ package com.juick.www.ad; import com.juick.model.User; -import com.juick.service.security.annotation.Visitor; import com.juick.www.controllers.Site; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -47,7 +46,7 @@ public class SapeService { @ModelAttribute public void addSapeLinks( - @Visitor User visitor, + @ModelAttribute User visitor, @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, @RequestParam(required = false, defaultValue = "0") int before, @RequestParam(name = "show", required = false) String paramShow, diff --git a/src/main/java/com/juick/www/api/Mastodon.java b/src/main/java/com/juick/www/api/Mastodon.java new file mode 100644 index 00000000..69f0f4f6 --- /dev/null +++ b/src/main/java/com/juick/www/api/Mastodon.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2008-2023, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.www.api; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.juick.model.User; +import com.juick.service.UserService; +import com.juick.www.WebApp; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.web.bind.annotation.*; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +@RestController +public class Mastodon { + @Value("${web_domain:localhost}") + private String domain; + @Inject + WebApp webApp; + @Inject + UserService userService; + @Inject + RegisteredClientRepository registeredClientRepository; + + public record ApplicationRequest(@JsonProperty("client_name") String clientName, + @JsonProperty("redirect_uris") String redirectUris, + @JsonProperty("scopes") String scopes) { + } + + public record ApplicationResponse(String id, + @JsonProperty("client_name") String name, + @JsonProperty("redirect_uri") String redirectUri, + @JsonProperty("client_id") String clientId, + @JsonProperty("client_secret") String clientSecret) { + } + + public record CredentialAccount(String id, String username, String acct, + @JsonProperty("display_name") String displayName, + @JsonProperty("followers_count") Integer followersCount, + @JsonProperty("following_count") Integer followingCount, + String avatar) { + + } + private Collection parseScopes(String s) { + return s != null ? Arrays.asList(s.split(" ")) : Collections.emptyList(); + } + + @PostMapping(value = "/api/v1/apps", consumes = { MediaType.APPLICATION_JSON_VALUE }) + public ApplicationResponse apps(@RequestBody ApplicationRequest application) { + return apps(application.clientName(), application.redirectUris(), application.scopes()); + } + + @PostMapping(value = "/api/v1/apps", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE }) + public ApplicationResponse apps(@RequestParam("client_name") String clientName, + @RequestParam("redirect_uris") String redirectUris, + @RequestParam("scopes") String scopes) { + var secret = UUID.randomUUID().toString(); + RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId(UUID.randomUUID().toString()) + .clientSecret("{noop}" + secret) + .clientName(clientName) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri(redirectUris) + .scopes((coll) -> coll.addAll(parseScopes(scopes))) + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build(); + registeredClientRepository.save(registeredClient); + return new ApplicationResponse( + registeredClient.getId(), + registeredClient.getClientName(), + String.join(",", registeredClient.getRedirectUris()), + registeredClient.getClientId(), + secret + ); + } + public record Instance(String domain) {} + @GetMapping("/api/v1/instance") + public Instance getInstance() { + return new Instance(domain); + } + @GetMapping("/api/v1/accounts/verify_credentials") + public CredentialAccount account(@ModelAttribute User visitor) { + return new CredentialAccount( + String.valueOf(visitor.getUid()), + visitor.getName(), + visitor.getName(), + visitor.getFullName(), + userService.getUserReaders(visitor.getUid()).size(), + userService.getUserFriends(visitor.getUid()).size(), + webApp.getAvatarUrl(visitor) + ); + } +} diff --git a/src/main/java/com/juick/www/api/Messages.java b/src/main/java/com/juick/www/api/Messages.java index 2993e805..c23976f4 100644 --- a/src/main/java/com/juick/www/api/Messages.java +++ b/src/main/java/com/juick/www/api/Messages.java @@ -30,14 +30,11 @@ import com.juick.service.MessagesService; import com.juick.service.TagService; import com.juick.service.UserService; import com.juick.service.component.SystemEvent; -import com.juick.service.security.annotation.Visitor; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; -import org.springframework.security.access.annotation.Secured; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; @@ -70,9 +67,8 @@ public class Messages { // TODO: serialize image urls - @GetMapping("/api/home") - @Secured("ROLE_USER") - public List getHome(@Visitor User visitor, + @GetMapping({"/api/home"}) + public List getHome(@ModelAttribute User visitor, @RequestParam(defaultValue = "0") int before_mid) { int vuid = visitor.getUid(); List mids = messagesService.getMyFeed(vuid, before_mid, true); @@ -82,7 +78,7 @@ public class Messages { } @GetMapping("/api/messages") - public List getMessages(@Visitor User visitor, + public List getMessages(@ModelAttribute User visitor, @RequestParam(required = false) String uname, @RequestParam(name = "before_mid", defaultValue = "0") Integer before, @RequestParam(required = false, defaultValue = "0") Integer daysback, @@ -140,7 +136,7 @@ public class Messages { } @DeleteMapping("/api/messages") - public CommandResult deleteMessage(@Visitor User visitor, @RequestParam int mid, + public CommandResult deleteMessage(@ModelAttribute User visitor, @RequestParam int mid, @RequestParam(required = false, defaultValue = "0") int rid) { if (rid > 0) { if (messagesService.deleteReply(visitor.getUid(), mid, rid)) { @@ -154,7 +150,7 @@ public class Messages { } @GetMapping("/api/messages/discussions") - public List getDiscussions(@Visitor User visitor, + public List getDiscussions(@ModelAttribute User visitor, @RequestParam(required = false, defaultValue = "0") Long to) { List msgs = messagesService.getMessages(visitor, messagesService.getDiscussions(visitor.getUid(), to)); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); @@ -162,7 +158,7 @@ public class Messages { } @GetMapping("/api/thread") - public List getThread(@Visitor User visitor, @RequestParam(defaultValue = "0") int mid, + public List getThread(@ModelAttribute User visitor, @RequestParam(defaultValue = "0") int mid, @RequestParam(defaultValue = "true") boolean showReplies) { Optional message = messagesService.getMessage(mid); if (message.isPresent()) { @@ -192,7 +188,7 @@ public class Messages { } @GetMapping(value = "/api/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE) - public byte[] markThreadRead(@Visitor User visitor, @PathVariable int mid, @PathVariable int rid) + public byte[] markThreadRead(@ModelAttribute User visitor, @PathVariable int mid, @PathVariable int rid) throws IOException { if (!visitor.isAnonymous()) { messagesService.setLastReadComment(visitor, mid, rid); diff --git a/src/main/java/com/juick/www/api/Notifications.java b/src/main/java/com/juick/www/api/Notifications.java index 09dad9e2..32ba3dc1 100644 --- a/src/main/java/com/juick/www/api/Notifications.java +++ b/src/main/java/com/juick/www/api/Notifications.java @@ -17,36 +17,19 @@ package com.juick.www.api; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -import com.juick.model.AnonymousUser; -import com.juick.model.ExternalToken; -import com.juick.model.Message; -import com.juick.model.Status; -import com.juick.model.User; -import com.juick.service.MessagesService; -import com.juick.service.PushQueriesService; -import com.juick.service.SubscriptionService; -import com.juick.service.TelegramService; -import com.juick.service.UserService; -import com.juick.service.security.annotation.Visitor; +import com.juick.model.*; +import com.juick.service.*; import com.juick.util.HttpBadRequestException; import com.juick.util.HttpForbiddenException; - +import io.swagger.v3.oas.annotations.Hidden; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import io.swagger.v3.oas.annotations.Hidden; +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; /** * Created by vitalyster on 24.10.2016. @@ -88,7 +71,7 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGet( - @Visitor User visitor, + @ModelAttribute(binding = false) User visitor, @RequestParam(required = false, defaultValue = "0") int uid, @RequestParam(required = false, defaultValue = "0") int mid, @RequestParam(required = false, defaultValue = "0") int rid) { @@ -127,24 +110,17 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public Status doDelete( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestBody List list) { if (!visitor.equals(serviceUser)) { throw new HttpForbiddenException(); } list.forEach(t -> { switch (t.type()) { - case "gcm": - pushQueriesService.deleteGCMToken(t.token()); - break; - case "apns": - pushQueriesService.deleteAPNSToken(t.token()); - break; - case "mpns": - pushQueriesService.deleteMPNSToken(t.token()); - break; - default: - throw new HttpBadRequestException(); + case "gcm" -> pushQueriesService.deleteGCMToken(t.token()); + case "apns" -> pushQueriesService.deleteAPNSToken(t.token()); + case "mpns" -> pushQueriesService.deleteMPNSToken(t.token()); + default -> throw new HttpBadRequestException(); } }); @@ -153,24 +129,17 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications/delete", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public Status doDeleteTokens( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestBody List list) { if (!visitor.equals(serviceUser)) { throw new HttpForbiddenException(); } list.forEach(t -> { switch (t.type()) { - case "gcm": - pushQueriesService.deleteGCMToken(t.token()); - break; - case "apns": - pushQueriesService.deleteAPNSToken(t.token()); - break; - case "mpns": - pushQueriesService.deleteMPNSToken(t.token()); - break; - default: - throw new HttpBadRequestException(); + case "gcm" -> pushQueriesService.deleteGCMToken(t.token()); + case "apns" -> pushQueriesService.deleteAPNSToken(t.token()); + case "mpns" -> pushQueriesService.deleteMPNSToken(t.token()); + default -> throw new HttpBadRequestException(); } }); @@ -180,21 +149,14 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public Status doPut( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestBody List list) { list.forEach(t -> { switch (t.type()) { - case "gcm": - pushQueriesService.addGCMToken(visitor.getUid(), t.token()); - break; - case "apns": - pushQueriesService.addAPNSToken(visitor.getUid(), t.token()); - break; - case "mpns": - pushQueriesService.addMPNSToken(visitor.getUid(), t.token()); - break; - default: - throw new HttpBadRequestException(); + case "gcm" -> pushQueriesService.addGCMToken(visitor.getUid(), t.token()); + case "apns" -> pushQueriesService.addAPNSToken(visitor.getUid(), t.token()); + case "mpns" -> pushQueriesService.addMPNSToken(visitor.getUid(), t.token()); + default -> throw new HttpBadRequestException(); } }); return Status.OK; @@ -203,7 +165,7 @@ public class Notifications { @Deprecated @RequestMapping(value = "/api/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public Status doAndroidRegister( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(name = "regid") String regId) { pushQueriesService.addGCMToken(visitor.getUid(), regId); return Status.OK; @@ -212,7 +174,7 @@ public class Notifications { @Deprecated @RequestMapping(value = "/api/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public Status doWinphoneRegister( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(name = "url") String regId) { pushQueriesService.addMPNSToken(visitor.getUid(), regId); return Status.OK; diff --git a/src/main/java/com/juick/www/api/PM.java b/src/main/java/com/juick/www/api/PM.java index 96387dce..c4acd4b3 100644 --- a/src/main/java/com/juick/www/api/PM.java +++ b/src/main/java/com/juick/www/api/PM.java @@ -29,13 +29,9 @@ import com.juick.www.WebApp; import com.juick.service.ChatService; import com.juick.service.UserService; import com.juick.service.component.SystemEvent; -import com.juick.service.security.annotation.Visitor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.Collections; @@ -57,7 +53,7 @@ public class PM { @RequestMapping(value = "/api/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetPM( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(required = false) String uname) { int uid = 0; if (uname != null && uname.matches("^[a-zA-Z0-9\\-]{2,16}$")) { @@ -75,7 +71,7 @@ public class PM { @RequestMapping(value = "/api/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public Message doPostPM( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam String uname, @RequestParam String body) { User userTo = AnonymousUser.INSTANCE; @@ -107,7 +103,7 @@ public class PM { } @RequestMapping(value = "/api/groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public PrivateChats doGetGroupsPMs( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(defaultValue = "5") int cnt) { // TODO: ignore cnt param for now but make sure paging param will not be cnt diff --git a/src/main/java/com/juick/www/api/Post.java b/src/main/java/com/juick/www/api/Post.java index c840a590..2a92178d 100644 --- a/src/main/java/com/juick/www/api/Post.java +++ b/src/main/java/com/juick/www/api/Post.java @@ -37,7 +37,6 @@ import com.juick.service.MessagesService; import com.juick.service.StorageService; import com.juick.service.UserService; import com.juick.service.activities.UpdateEvent; -import com.juick.service.security.annotation.Visitor; import com.juick.util.HttpBadRequestException; import com.juick.util.HttpForbiddenException; import com.juick.util.HttpNotFoundException; @@ -49,13 +48,7 @@ import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; /** @@ -81,7 +74,7 @@ public class Post { @RequestMapping(value = "/api/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(value = HttpStatus.OK) public CommandResult doPostMessage( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body, @RequestParam(required = false) String img, @RequestParam(required = false) MultipartFile attach) throws Exception { @@ -112,7 +105,7 @@ public class Post { @RequestMapping(value = "/api/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public CommandResult doPostComment( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(defaultValue = "0") int mid, @RequestParam(defaultValue = "0") int rid, @RequestParam(required = false, defaultValue = StringUtils.EMPTY) final String body, @@ -164,7 +157,7 @@ public class Post { @PostMapping("/api/like") @ResponseStatus(value = HttpStatus.OK) - public Status doPostRecommendation(@Visitor User visitor, @RequestParam Integer mid) throws Exception { + public Status doPostRecommendation(@ModelAttribute User visitor, @RequestParam Integer mid) throws Exception { Optional message = messagesService.getMessage(mid); if (message.isEmpty()) { throw new HttpNotFoundException(); @@ -180,7 +173,7 @@ public class Post { @PostMapping("/api/subscribe") @ResponseStatus(value = HttpStatus.OK) - public Status doPostSubscribe(@Visitor User visitor, + public Status doPostSubscribe(@ModelAttribute User visitor, @RequestParam Integer mid) throws Exception { Optional message = messagesService.getMessage(mid); if (message.isEmpty()) { @@ -204,7 +197,7 @@ public class Post { @PostMapping("/api/react") @ResponseStatus(value = HttpStatus.OK) public Status doPostReact( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam Integer mid, @RequestParam @NotNull int reactionId, @RequestParam(required = false, defaultValue = "1") int count) { @@ -226,7 +219,7 @@ public class Post { } @PostMapping("/api/update") - public CommandResult updateMessage(@Visitor User visitor, + public CommandResult updateMessage(@ModelAttribute User visitor, @RequestParam Integer mid, @RequestParam(required = false, defaultValue = "0") Integer rid, @RequestParam String body) { diff --git a/src/main/java/com/juick/www/api/Service.java b/src/main/java/com/juick/www/api/Service.java index 3bb760ff..f4599a56 100644 --- a/src/main/java/com/juick/www/api/Service.java +++ b/src/main/java/com/juick/www/api/Service.java @@ -30,7 +30,6 @@ import com.juick.service.MessagesService; import com.juick.service.StorageService; import com.juick.service.UserService; import com.juick.service.component.AccountVerificationEvent; -import com.juick.service.security.annotation.Visitor; import io.swagger.v3.oas.annotations.Hidden; import jakarta.mail.Session; import jakarta.mail.internet.AddressException; @@ -49,10 +48,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -92,7 +88,7 @@ public class Service { @Hidden @PostMapping("/api/mail") @ResponseStatus(value = HttpStatus.OK) - public void processMail(@Visitor User current, InputStream data) throws Exception { + public void processMail(@ModelAttribute User current, InputStream data) throws Exception { if (current.equals(serviceUser)) { MimeMessage msg = new MimeMessage(session, data); String[] returnPaths = msg.getHeader("Return-Path"); @@ -207,7 +203,7 @@ public class Service { @Hidden @PostMapping("/api/mail/unsubscribe") @ResponseStatus(value = HttpStatus.OK) - public void processMailUnsubscribe(@Visitor User current, InputStream data) throws Exception { + public void processMailUnsubscribe(@ModelAttribute User current, InputStream data) throws Exception { if (current.equals(serviceUser)) { MimeMessage msg = new MimeMessage(session, data); String from = msg.getFrom() == null || msg.getFrom().length > 1 @@ -231,7 +227,7 @@ public class Service { } @GetMapping("/api/events") - public SseEmitter handle(@Visitor User visitor) { + public SseEmitter handle(@ModelAttribute User visitor) { logger.info("{} connected", visitor.getName()); if (!visitor.isAnonymous()) { userService.updateLastSeen(visitor); diff --git a/src/main/java/com/juick/www/api/Tags.java b/src/main/java/com/juick/www/api/Tags.java index 7d934f38..2b6405ac 100644 --- a/src/main/java/com/juick/www/api/Tags.java +++ b/src/main/java/com/juick/www/api/Tags.java @@ -20,12 +20,8 @@ package com.juick.www.api; import com.juick.model.User; import com.juick.model.TagStats; import com.juick.service.TagService; -import com.juick.service.security.annotation.Visitor; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.List; @@ -40,7 +36,7 @@ public class Tags { @RequestMapping(value = "/api/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List tags( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(required = false, defaultValue = "0") int user_id ) { if (user_id > 0) { diff --git a/src/main/java/com/juick/www/api/Users.java b/src/main/java/com/juick/www/api/Users.java index dd620380..f7c24d8d 100644 --- a/src/main/java/com/juick/www/api/Users.java +++ b/src/main/java/com/juick/www/api/Users.java @@ -36,7 +36,6 @@ import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.service.activities.UpdateUserEvent; import com.juick.service.component.MailVerificationEvent; -import com.juick.service.security.annotation.Visitor; import com.juick.util.HttpBadRequestException; import com.juick.util.HttpNotFoundException; import com.juick.util.HttpUtils; @@ -48,13 +47,7 @@ import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; /** @@ -80,13 +73,13 @@ public class Users { private ApplicationEventPublisher applicationEventPublisher; @RequestMapping(value = "/api/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) - public String getAuthToken(@Visitor User visitor) { + public String getAuthToken(@ModelAttribute User visitor) { return userService.getHashByUID(visitor.getUid()); } @RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetUsers( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(value = "uname", required = false) List unames) { List users = new ArrayList<>(); @@ -108,7 +101,7 @@ public class Users { } @GetMapping("/api/me") - public SecureUser getMe(@Visitor User visitor) { + public SecureUser getMe(@ModelAttribute User visitor) { SecureUser me = new SecureUser(); me.setUid(visitor.getUid()); me.setName(visitor.getName()); @@ -127,7 +120,7 @@ public class Users { return (SecureUser)userService.getUserInfo(me); } @PostMapping("/api/me") - public void updateMe(@Visitor User visitor, + public void updateMe(@ModelAttribute User visitor, @RequestParam(required = false) String password, @RequestParam(value = "jid-del", required = false) String jidForDeletion, @RequestParam(value = "email-add", required = false) String newEmail, @@ -171,12 +164,12 @@ public class Users { } } @PostMapping("/api/me/subscribe") - public void subscribeMe(@Visitor User visitor, String email) { + public void subscribeMe(@ModelAttribute User visitor, String email) { // TODO: check status emailService.setNotificationsEmail(visitor.getUid(), email); } @PostMapping("/api/me/upload") - public void updateInfo(@Visitor User visitor, + public void updateInfo(@ModelAttribute User visitor, @RequestParam MultipartFile avatar) throws IOException { String avatarTmpPath = HttpUtils.receiveMultiPartFile(avatar, storageService.getTemporaryDirectory()).getHost(); if (StringUtils.isNotEmpty(avatarTmpPath)) { @@ -187,7 +180,7 @@ public class Users { @RequestMapping(value = "/api/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetUserRead( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam String uname) { int uid = 0; if (uname == null) { @@ -211,7 +204,7 @@ public class Users { @RequestMapping(value = "/api/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetUserReaders( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam String uname) { int uid = 0; if (uname == null) { @@ -234,7 +227,7 @@ public class Users { } @GetMapping("/api/info/{uname}") - public User getUserInfo(@Visitor User visitor, @PathVariable String uname) { + public User getUserInfo(@ModelAttribute User visitor, @PathVariable String uname) { User user = userService.getUserByName(uname); if (!user.isBanned()) { user.setRead(doGetUserRead(visitor, uname)); diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java index 404e0734..7a3fdf29 100644 --- a/src/main/java/com/juick/www/api/activity/Profile.java +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -48,7 +48,6 @@ import com.juick.service.activities.DirectMessageEvent; import com.juick.service.activities.FollowEvent; import com.juick.service.activities.UndoAnnounceEvent; import com.juick.service.activities.UndoFollowEvent; -import com.juick.service.security.annotation.Visitor; import com.overzealous.remark.Remark; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -61,11 +60,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; @@ -136,8 +131,8 @@ public class Profile { @GetMapping(value = "/u/{userName}/blog", produces = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE }) - public OrderedCollectionPage getOutboxPage(@Visitor User visitor, @PathVariable String userName, - @RequestParam(required = false, defaultValue = "0") int before) { + public OrderedCollectionPage getOutboxPage(@ModelAttribute User visitor, @PathVariable String userName, + @RequestParam(required = false, defaultValue = "0") int before) { User user = userService.getUserByName(userName); if (!user.isAnonymous() && !user.isBanned()) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); @@ -273,7 +268,7 @@ public class Profile { @CacheEvict(cacheNames = "profiles", key = "{ #visitor.uri }") @PostMapping(value = "/api/inbox", consumes = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE }) - public ResponseEntity processInbox(@Visitor User visitor, InputStream inboxData) throws Exception { + public ResponseEntity processInbox(@ModelAttribute User visitor, InputStream inboxData) throws Exception { String inbox = IOUtils.toString(inboxData, StandardCharsets.UTF_8); Activity activity = jsonMapper.readValue(inbox, Activity.class); if ((StringUtils.isNotEmpty(visitor.getUri().toString()) diff --git a/src/main/java/com/juick/www/controllers/Help.java b/src/main/java/com/juick/www/controllers/Help.java index ae1dafbe..ae7ba9d1 100644 --- a/src/main/java/com/juick/www/controllers/Help.java +++ b/src/main/java/com/juick/www/controllers/Help.java @@ -20,13 +20,13 @@ package com.juick.www.controllers; import com.juick.model.User; import com.juick.util.HttpNotFoundException; import com.juick.service.HelpService; -import com.juick.service.security.annotation.Visitor; import com.juick.www.WebApp; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import javax.inject.Inject; @@ -49,7 +49,7 @@ public class Help { @GetMapping({"/help/", "/help", "/help/{langOrPage}", "/help/{lang}/{page}"}) public String showHelp( - @Visitor User visitor, + @ModelAttribute User visitor, Locale locale, @PathVariable(required = false, name = "lang") String lang, @PathVariable(required = false, name = "page") String page, diff --git a/src/main/java/com/juick/www/controllers/Settings.java b/src/main/java/com/juick/www/controllers/Settings.java index b990bf41..4d7deece 100644 --- a/src/main/java/com/juick/www/controllers/Settings.java +++ b/src/main/java/com/juick/www/controllers/Settings.java @@ -35,7 +35,6 @@ import com.juick.service.TagService; import com.juick.service.TelegramService; import com.juick.service.UserService; import com.juick.service.activities.UpdateUserEvent; -import com.juick.service.security.annotation.Visitor; import com.juick.util.HttpBadRequestException; import com.juick.util.HttpUtils; import com.juick.www.WebApp; @@ -58,6 +57,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; @@ -89,7 +89,7 @@ public class Settings { @GetMapping("/settings") protected String doGet( - @Visitor User visitor, + @ModelAttribute User visitor, Locale locale, @RequestParam(required = false, defaultValue = "main") String page, @RequestParam(required = false) String code, ModelMap model) throws IOException { @@ -126,7 +126,7 @@ public class Settings { @PostMapping("/settings") protected String doPost( - @Visitor User visitor, + @ModelAttribute User visitor, HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) MultipartFile avatar, ModelMap model) @@ -272,7 +272,7 @@ public class Settings { } @PostMapping("/settings/unsubscribe") public String unsubscribeOneClick( - @Visitor User user, + @ModelAttribute User user, @RequestParam(name = "List-Unsubscribe") String unsubscribe, ModelMap model) { if (!user.isAnonymous()) { diff --git a/src/main/java/com/juick/www/controllers/SignUp.java b/src/main/java/com/juick/www/controllers/SignUp.java index 9fc04dd5..8318dabd 100644 --- a/src/main/java/com/juick/www/controllers/SignUp.java +++ b/src/main/java/com/juick/www/controllers/SignUp.java @@ -23,13 +23,13 @@ import com.juick.util.UsernameTakenException; import com.juick.www.WebApp; import com.juick.service.EmailService; import com.juick.service.UserService; -import com.juick.service.security.annotation.Visitor; import com.juick.service.security.entities.JuickUser; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -52,7 +52,7 @@ public class SignUp { @GetMapping("/signup") protected String doGet( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam String type, @RequestParam String hash, ModelMap model) { if (hash.length() > 36 || !type.matches("^[a-zA-Z0-9\\-]+$") || !hash.matches("^[a-zA-Z0-9\\-]+$")) { @@ -91,7 +91,7 @@ public class SignUp { @PostMapping("/signup") protected String doPost( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam String type, @RequestParam String hash, @RequestParam String action, diff --git a/src/main/java/com/juick/www/controllers/Site.java b/src/main/java/com/juick/www/controllers/Site.java index f45fe8f2..e8acc650 100644 --- a/src/main/java/com/juick/www/controllers/Site.java +++ b/src/main/java/com/juick/www/controllers/Site.java @@ -30,7 +30,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import com.juick.service.*; -import com.juick.service.security.annotation.Visitor; import com.juick.util.MessageUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -43,12 +42,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.WebAttributes; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.net.URLEncoder; @@ -99,7 +93,7 @@ public class Site { } @GetMapping("/login") - public String getloginForm(@Visitor User visitor, + public String getloginForm(@ModelAttribute User visitor, @RequestParam(name = "retpath", required = false, defaultValue = "/") String retPath, HttpSession session, ModelMap model) { @@ -125,7 +119,7 @@ public class Site { } @GetMapping("/") - protected String doGet(@Visitor User visitor, Locale locale, @RequestParam(required = false) String tag, + protected String doGet(@ModelAttribute User visitor, Locale locale, @RequestParam(required = false) String tag, @RequestParam(name = "show", required = false) String paramShow, @RequestParam(name = "search", required = false) String paramSearch, @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore, @@ -217,7 +211,7 @@ public class Site { } @GetMapping(path = "/{uname}/", headers = "Connection!=Upgrade") - protected String doGetBlog(@Visitor User visitor, @RequestParam(required = false, name = "show") String paramShow, + protected String doGetBlog(@ModelAttribute User visitor, @RequestParam(required = false, name = "show") String paramShow, @RequestParam(required = false, name = "tag") String paramTagStr, @RequestParam(required = false, name = "search") String paramSearch, @RequestParam(required = false, name = "page", defaultValue = "0") Integer page, @PathVariable String uname, @@ -324,7 +318,7 @@ public class Site { } @GetMapping("/{uname}/tags") - protected String doGetTags(@Visitor User visitor, @PathVariable String uname, ModelMap model) { + protected String doGetTags(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); if (visitor.isBanned()) { throw new HttpNotFoundException(); @@ -344,7 +338,7 @@ public class Site { } @GetMapping("/{uname}/friends") - protected String doGetFriends(@Visitor User visitor, @PathVariable String uname, ModelMap model) { + protected String doGetFriends(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); if (visitor.isBanned()) { throw new HttpNotFoundException(); @@ -360,7 +354,7 @@ public class Site { } @GetMapping("/{uname}/readers") - protected String doGetReaders(@Visitor User visitor, @PathVariable String uname, ModelMap model) { + protected String doGetReaders(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Читатели " + user.getName()); @@ -373,7 +367,7 @@ public class Site { } @GetMapping("/{uname}/bl") - protected String doGetBL(@Visitor User visitor, @PathVariable String uname, ModelMap model) { + protected String doGetBL(@ModelAttribute User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); if (visitor.getUid() != user.getUid()) { throw new HttpForbiddenException(); @@ -389,7 +383,7 @@ public class Site { } @GetMapping("/tag/{tagName}") - protected String tagAction(@Visitor User visitor, HttpServletRequest request, @PathVariable String tagName, + protected String tagAction(@ModelAttribute User visitor, HttpServletRequest request, @PathVariable String tagName, @RequestParam(required = false, defaultValue = "0") int before, ModelMap model) { visitor.setAvatar(webApp.getAvatarWebPath(visitor)); String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName); @@ -452,7 +446,7 @@ public class Site { } @GetMapping("/pm/inbox") - protected String doGetInbox(@Visitor User visitor, ModelMap model) { + protected String doGetInbox(@ModelAttribute User visitor, ModelMap model) { visitor.setAvatar(webApp.getAvatarWebPath(visitor)); String title = "PM: Inbox"; List msgs = chatService.getInbox(visitor.getUid()); @@ -466,7 +460,7 @@ public class Site { } @GetMapping("/pm/sent") - protected String doGetSent(@Visitor User visitor, @RequestParam(required = false) String uname, ModelMap model) { + protected String doGetSent(@ModelAttribute User visitor, @RequestParam(required = false) String uname, ModelMap model) { visitor.setAvatar(webApp.getAvatarWebPath(visitor)); String title = "PM: Sent"; List msgs = chatService.getOutbox(visitor.getUid()); @@ -485,7 +479,7 @@ public class Site { @GetMapping(value = "/{uname}/{mid}", produces = { MediaType.TEXT_HTML_VALUE, Context.ACTIVITY_MEDIA_TYPE, Context.LD_JSON_MEDIA_TYPE }) - protected String threadAction(@Visitor User visitor, ModelMap model, @PathVariable String uname, + protected String threadAction(@ModelAttribute User visitor, ModelMap model, @PathVariable String uname, @PathVariable int mid, @RequestHeader(name = HttpHeaders.ACCEPT, required = false) String acceptHeader, @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) { @@ -589,7 +583,7 @@ public class Site { } @GetMapping("/post") - protected String postAction(@Visitor User visitor, @RequestParam(required = false) String body, ModelMap model) { + protected String postAction(@ModelAttribute User visitor, @RequestParam(required = false) String body, ModelMap model) { fillUserModel(model, visitor, visitor); visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Написать"); diff --git a/src/main/java/com/juick/www/controllers/SocialLogin.java b/src/main/java/com/juick/www/controllers/SocialLogin.java index c9611543..24bf97f6 100644 --- a/src/main/java/com/juick/www/controllers/SocialLogin.java +++ b/src/main/java/com/juick/www/controllers/SocialLogin.java @@ -27,7 +27,6 @@ import com.juick.model.ext.vk.UsersResponse; import com.juick.service.EmailService; import com.juick.service.TelegramService; import com.juick.service.UserService; -import com.juick.service.security.annotation.Visitor; import com.juick.util.HttpBadRequestException; import jakarta.annotation.PostConstruct; @@ -46,11 +45,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; import javax.inject.Inject; @@ -191,8 +186,8 @@ public class SocialLogin { } @GetMapping("/_twitter") - protected void doTwitterLogin(@Visitor com.juick.model.User user, HttpServletRequest request, - HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { + protected void doTwitterLogin(@ModelAttribute com.juick.model.User user, HttpServletRequest request, + HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY; String verifier = request.getParameter("oauth_verifier"); Cookie[] cookies = request.getCookies(); diff --git a/src/main/java/com/juick/www/rss/Feeds.java b/src/main/java/com/juick/www/rss/Feeds.java index 8111f2df..41b006a6 100644 --- a/src/main/java/com/juick/www/rss/Feeds.java +++ b/src/main/java/com/juick/www/rss/Feeds.java @@ -21,16 +21,12 @@ import com.juick.model.User; import com.juick.util.HttpNotFoundException; import com.juick.service.MessagesService; import com.juick.service.UserService; -import com.juick.service.security.annotation.Visitor; import org.springframework.core.convert.ConversionFailedException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.inject.Inject; @@ -53,7 +49,7 @@ public class Feeds { } @GetMapping("/rss/{userName}/{feedType}") - public ModelAndView getBlog(@Visitor User visitor, @PathVariable String userName, @PathVariable FeedType feedType) { + public ModelAndView getBlog(@ModelAttribute User visitor, @PathVariable String userName, @PathVariable FeedType feedType) { User user = userService.getUserByName(userName); if (!user.isAnonymous() && !user.isBanned()) { List mids = feedType == FeedType.blog ? messagesService.getUserBlog(user.getUid(), 0, 0) : messagesService.getUserBlogWithRecommendations(user, visitor, 0, 0); @@ -69,7 +65,7 @@ public class Feeds { @GetMapping("/rss/") public ModelAndView getLast( - @Visitor User visitor, + @ModelAttribute User visitor, @RequestParam(value = "hours", required = false, defaultValue = "0") Integer hours) { List mids = messagesService.getLastMessages(hours); ModelAndView modelAndView = new ModelAndView(); diff --git a/src/main/java/com/juick/www/rss/MessagesView.java b/src/main/java/com/juick/www/rss/MessagesView.java index 71d63754..d6edeb28 100644 --- a/src/main/java/com/juick/www/rss/MessagesView.java +++ b/src/main/java/com/juick/www/rss/MessagesView.java @@ -93,37 +93,39 @@ public class MessagesView extends AbstractRssFeedView { String feedType = (String) model.get("feedType"); if (userObj != null) { User user = (User) userObj; - feed.setDescription(String.format("The latest messages by @%s at Juick", user.getName())); - String title = String.format("%s - Juick", user.getName()); - feed.setTitle(title); - String link = String.format("https://juick.com/%s/", user.getName()); - feed.setLink(link); - try { - Attachment avatar = storageService.getAvatarMetadata(user); - Image rssImage = new Image(); - rssImage.setUrl(webApp.getAvatarUrl(user)); - rssImage.setTitle(title); - rssImage.setLink(link); - rssImage.setHeight(avatar.getHeight()); - rssImage.setWidth(avatar.getWidth()); - feed.setImage(rssImage); - } catch (IOException e) { - logger.warn("Feed avatar not found for {}", user.getName()); + if (!user.isAnonymous()) { + feed.setDescription(String.format("The latest messages by @%s at Juick", user.getName())); + String title = String.format("%s - Juick", user.getName()); + feed.setTitle(title); + String link = String.format("https://juick.com/%s/", user.getName()); + feed.setLink(link); + try { + Attachment avatar = storageService.getAvatarMetadata(user); + Image rssImage = new Image(); + rssImage.setUrl(webApp.getAvatarUrl(user)); + rssImage.setTitle(title); + rssImage.setLink(link); + rssImage.setHeight(avatar.getHeight()); + rssImage.setWidth(avatar.getWidth()); + feed.setImage(rssImage); + } catch (IOException e) { + logger.warn("Feed avatar not found for {}", user.getName()); + } + + String href = String.format("https://rss.juick.com/%s/%s", user.getName(), feedType); + AtomLinkModule atomLinkModule = new AtomLinkModuleImpl(); + Link atomLink = new Link(); + atomLink.setHref(href); + atomLink.setType("application/rss+xml"); + atomLink.setRel("self"); + atomLinkModule.setLinks(Collections.singletonList(atomLink)); + + feed.getModules().add(atomLinkModule); + } else { + feed.setDescription("The latest messages at Juick"); + feed.setLink("https://juick.com/"); + feed.setTitle("Juick"); } - - String href = String.format("https://rss.juick.com/%s/%s", user.getName(), feedType); - AtomLinkModule atomLinkModule = new AtomLinkModuleImpl(); - Link atomLink = new Link(); - atomLink.setHref(href); - atomLink.setType("application/rss+xml"); - atomLink.setRel("self"); - atomLinkModule.setLinks(Collections.singletonList(atomLink)); - - feed.getModules().add(atomLinkModule); - } else { - feed.setDescription("The latest messages at Juick"); - feed.setLink("https://juick.com/"); - feed.setTitle("Juick"); } MediaModule mediaModule = new MediaModuleImpl(); diff --git a/src/main/resources/db/migration/V1.29__oauth_clients.sql b/src/main/resources/db/migration/V1.29__oauth_clients.sql new file mode 100644 index 00000000..17bec0b3 --- /dev/null +++ b/src/main/resources/db/migration/V1.29__oauth_clients.sql @@ -0,0 +1,15 @@ +CREATE TABLE oauth2_registered_client ( + id varchar(100) NOT NULL, + client_id varchar(100) NOT NULL, + client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + client_secret varchar(200) DEFAULT NULL, + client_secret_expires_at timestamp DEFAULT NULL, + client_name varchar(200) NOT NULL, + client_authentication_methods varchar(1000) NOT NULL, + authorization_grant_types varchar(1000) NOT NULL, + redirect_uris varchar(1000) DEFAULT NULL, + scopes varchar(1000) NOT NULL, + client_settings varchar(2000) NOT NULL, + token_settings varchar(2000) NOT NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java index 957da377..5eeeab3d 100644 --- a/src/test/java/com/juick/server/tests/ServerTests.java +++ b/src/test/java/com/juick/server/tests/ServerTests.java @@ -90,11 +90,13 @@ import org.springframework.core.io.Resource; import org.springframework.http.*; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.DigestUtils; @@ -2717,6 +2719,19 @@ public class ServerTests { assertThat(descr.getDescription(), equalTo("Test")); } + @Test + public void givenAccessSecuredResource_whenAuthenticated_thenRedirectedBack() throws Exception { + MockHttpServletRequestBuilder securedResourceAccess = get("/settings"); + MvcResult unauthenticatedResult = mockMvc.perform(securedResourceAccess).andExpect(status().is3xxRedirection()) + .andReturn(); + MockHttpSession session = (MockHttpSession) unauthenticatedResult.getRequest().getSession(); + String loginUrl = unauthenticatedResult.getResponse().getRedirectedUrl(); + mockMvc.perform(post(loginUrl).param("username", ugnichName).param("password", ugnichPassword) + .session(session).with(csrf())).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/settings?continue")).andReturn(); + mockMvc.perform(securedResourceAccess.session(session)).andExpect(status().isOk()); + } +/* @Test public void tokenAuth() throws Exception { var token = keystoreManager.generateToken(ugnich); @@ -2732,4 +2747,6 @@ public class ServerTests { .header("Authorization", "Bearer " + token)) .andExpect(status().isUnauthorized()); } + + */ } -- cgit v1.2.3