diff options
Diffstat (limited to 'src/main')
7 files changed, 144 insertions, 56 deletions
diff --git a/src/main/assets/style.css b/src/main/assets/style.css index 99cefffa6..00f92ef3b 100644 --- a/src/main/assets/style.css +++ b/src/main/assets/style.css @@ -392,12 +392,12 @@ article .ir a { display: block; } -article .ir img { +article .ir img, .msg-media img { max-width: 100%; } article > nav.l, .msg-cont > nav.l { - border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); display: flex; align-items: center; justify-content: space-around; @@ -490,8 +490,16 @@ article .tags > a::before, .msg-cont { background: var(--text-background-color); border: 1px solid var(--border-color); + border-bottom: 1px solid transparent; line-height: 140%; - margin-bottom: 12px; +} + +#replies .msg-cont { + border-top: 1px solid transparent; +} + +#replies .msg-cont:last-child { + border-bottom: 1px solid var(--border-color); } .reply-new .msg-cont { @@ -580,22 +588,6 @@ article .tags > a::before, margin: 0; } -.title2 { - background: var(--text-background-color); - margin: 20px 0; - padding: 10px 20px; -} - -.title2-right { - float: right; - line-height: 24px; -} - -#content .title2 h2 { - font-size: x-large; - margin: 0; -} - /* #endregion */ /* #region user-generated texts */ diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java index a93a4a5ca..7e37b7d8e 100644 --- a/src/main/java/com/juick/config/SecurityConfig.java +++ b/src/main/java/com/juick/config/SecurityConfig.java @@ -35,6 +35,7 @@ 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.http.HttpStatus; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -52,10 +53,10 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; -import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; @@ -115,9 +116,7 @@ public class SecurityConfig { @Bean AuthenticationEntryPoint apiAuthenticationEntryPoint() { - var entryPoint = new BasicAuthenticationEntryPoint(); - entryPoint.setRealmName("Juick"); - return entryPoint; + return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); } @Value("${auth_remember_me_key:secret}") @@ -201,7 +200,7 @@ public class SecurityConfig { "/api/swagger-ui/**", "/api/messages/discussions", "/api/users", "/api/thread", "/api/tags", - "/api/tlgmbtwbhk", "/api/fbwbhk", "/api/_patreon", "/api/_vk", + "/api/tlgmbtwbhk", "/api/fbwbhk", "/api/_patreon", "/api/_vk", "/api/_google", "/api/skypebotendpoint", "/api/signup", "/api/inbox", "/api/events", "/api/u/", "/u/**", "/n/**", @@ -228,6 +227,7 @@ public class SecurityConfig { } @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) SecurityFilterChain h2ConsoleFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/h2-console/**") .authorizeHttpRequests(auth -> auth @@ -294,6 +294,7 @@ public class SecurityConfig { } @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) public SecurityFilterChain securityWebFilterChain( HttpSecurity http) throws Exception { return http.securityMatcher("/actuator/**") diff --git a/src/main/java/com/juick/service/MessagesServiceImpl.java b/src/main/java/com/juick/service/MessagesServiceImpl.java index de3429774..ebb8414e2 100644 --- a/src/main/java/com/juick/service/MessagesServiceImpl.java +++ b/src/main/java/com/juick/service/MessagesServiceImpl.java @@ -18,7 +18,6 @@ package com.juick.service; import com.juick.model.*; -import com.juick.model.User; import com.juick.www.WebApp; import com.juick.util.MessageUtils; import com.juick.util.TagUtils; @@ -103,16 +102,20 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ msg.setReplyQuote(MessageUtils.formatQuote(rs.getString(17))); msg.setUpdated(MessagesServiceImpl.this.getOffsetDateTime(rs, 18).toInstant()); int quoteUid = rs.getInt(19); - User quoteUser = new User(); - quoteUser.setUid(quoteUid); - quoteUser.setName(rs.getString(20)); - if (quoteUid == 0) { - quoteUser.setName(AnonymousUser.INSTANCE.getName()); - quoteUser.setUri(URI.create(Optional.ofNullable(rs.getString(23)).orElse(StringUtils.EMPTY))); + if (rs.wasNull()) { + msg.setTo(archiveUser); } else { - quoteUser.setAvatar(webApp.getAvatarUrl(quoteUser)); + User quoteUser = new User(); + quoteUser.setUid(quoteUid); + quoteUser.setName(rs.getString(20)); + if (quoteUid == 0) { + quoteUser.setName(AnonymousUser.INSTANCE.getName()); + quoteUser.setUri(URI.create(Optional.ofNullable(rs.getString(23)).orElse(StringUtils.EMPTY))); + } else { + quoteUser.setAvatar(webApp.getAvatarUrl(quoteUser)); + } + msg.setTo(quoteUser); } - msg.setTo(quoteUser); msg.setUpdatedAt(MessagesServiceImpl.this.getOffsetDateTime(rs, 21).toInstant()); msg.setReplyUri(URI.create(Optional.ofNullable(rs.getString(24)).orElse(StringUtils.EMPTY))); msg.setHtml(rs.getBoolean(25)); @@ -897,7 +900,7 @@ public class MessagesServiceImpl extends BaseJdbcService implements MessagesServ + "replies.user_id, users.nick, users.banned, " + "replies.ts, " + "0 as readonly, 0 as privacy, 0 as replies, " + "replies.attach, 0 as likes, 0 as hidden, " + "NULL as tags, NULL as repliesby, replies.txt, " + "COALESCE(qw.txt, m.txt) as q, " + ":now, " - + "COALESCE(qw.user_id, m.user_id) as to_uid, COALESCE(qu.nick, mu.nick) as to_name, " + + "qw.user_id as to_uid, qu.nick as to_name, " + "replies.updated_at, replies.user_uri as uri, " + "qw.user_uri as to_uri, replies.reply_uri, replies.html, 0 as unread, " + "0 as subscribed, users.premium " diff --git a/src/main/java/com/juick/util/MessageUtils.java b/src/main/java/com/juick/util/MessageUtils.java index 3c8001d81..fff97f662 100644 --- a/src/main/java/com/juick/util/MessageUtils.java +++ b/src/main/java/com/juick/util/MessageUtils.java @@ -42,13 +42,20 @@ public class MessageUtils { } public static String formatQuote(final String quote) { + return formatQuote(quote, false); + } + + public static String formatQuote(final String quote, final boolean isHtml) { String result = quote; + var prefix = isHtml ? "<blockquote>" : ">"; + var suffix = isHtml ? "</blockquote>" : "\n"; + if (quote != null) { if (quote.length() > 50) { - result = ">" + StringUtils.abbreviate(quote, "…", 47).replace('\n', ' ') + "\n"; + result = prefix + StringUtils.abbreviate(quote, "…", 47).replace('\n', ' ') + suffix; } else if (!quote.isEmpty()) { - result = ">" + quote.replace('\n', ' ') + "\n"; + result = prefix + quote.replace('\n', ' ') + suffix; } } @@ -191,8 +198,9 @@ public class MessageUtils { // /12 // <a href="#12">/12</a> - msg = msg.replaceAll(replyNumberRegex, "$1<a href=\"#$2\">/$2</a>$3"); - + if (!compatibleWithDurov) { + msg = msg.replaceAll(replyNumberRegex, "$1<a href=\"#$2\">/$2</a>$3"); + } // @username@mastodon.social // <a href="http://juick.com/mention?username=username@mastodon.social/">@username@mastodon.social</a> @@ -220,11 +228,11 @@ public class MessageUtils { m.appendTail(sb); msg = sb.toString(); + + // > citate + msg = msg.replaceAll(citateRegex, "<blockquote>$1</blockquote>"); + msg = msg.replaceAll("</blockquote><blockquote>", "\n"); if (!compatibleWithDurov) { - // > citate - msg = msg.replaceAll(citateRegex, "<blockquote>$1</blockquote>"); - msg = msg.replaceAll("</blockquote><blockquote>", "\n"); - msg = msg.replaceAll("\n", "<br/>\n"); } return msg; @@ -283,10 +291,6 @@ public class MessageUtils { return URLEncoder.encode(s, StandardCharsets.UTF_8).replace("+", "%20") .replace("*", "%2A").replace("%7E", "~"); } - public static String formatMarkdownText(final Message msg) { - return StringUtils.defaultString(msg.getText()) - .replaceAll(replyNumberRegex, String.format("$1[/$2](https://juick.com/m/%d#$2)$3", msg.getMid())); - } public static String attachmentUrl(final Message jmsg) { if (StringUtils.isEmpty(jmsg.getAttachmentType())) { return StringUtils.EMPTY; diff --git a/src/main/java/com/juick/www/api/ApiSocialLogin.java b/src/main/java/com/juick/www/api/ApiSocialLogin.java new file mode 100644 index 000000000..5b48c52be --- /dev/null +++ b/src/main/java/com/juick/www/api/ApiSocialLogin.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008-2024, 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 <http://www.gnu.org/licenses/>. + */ +package com.juick.www.api; + +import com.github.scribejava.apis.GoogleTokenVerifier; +import com.juick.model.AuthResponse; +import com.juick.service.EmailService; +import com.juick.service.UserService; +import com.juick.util.HttpBadRequestException; +import com.juick.util.HttpForbiddenException; + +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import jakarta.inject.Inject; +import java.util.Optional; + +@Controller +public class ApiSocialLogin { + + private static final Logger logger = LoggerFactory.getLogger(ApiSocialLogin.class); + + @Value("${google_client_id:}") + private String googleClientId; + + @Inject + private UserService userService; + @Inject + private EmailService emailService; + @Inject + private Users users; + @ResponseBody + @PostMapping("/api/signup") + public com.juick.model.User signupWithEmail(String username, String password, String verificationCode) { + if (username.length() < 2 || username.length() > 16 || !username.matches("^[a-zA-Z0-9\\-]+$") + || password.length() < 6 || password.length() > 32) { + throw new HttpBadRequestException(); + } + + String verifiedEmail = emailService.getEmailByAuthCode(verificationCode); + if (StringUtils.hasText(verifiedEmail)) { + com.juick.model.User newUser = userService.createUser(username, password).orElseThrow(HttpBadRequestException::new); + emailService.addEmail(newUser.getUid(), verifiedEmail); + emailService.deleteAuthCode(verificationCode); + return newUser; + } else { + throw new HttpForbiddenException(); + } + } + @ResponseBody + @PostMapping("/api/_google") + public AuthResponse googleSignIn(@RequestParam(name = "idToken") String idTokenString) { + logger.info("Token: {}", idTokenString); + logger.info("Client: {}", googleClientId); + Optional<String> verifiedEmail = GoogleTokenVerifier.validateToken(googleClientId, idTokenString); + if (verifiedEmail.isPresent()) { + String email = verifiedEmail.get(); + com.juick.model.User visitor = userService.getUserByEmail(email); + if (visitor.isAnonymous()) { + String verificationCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + emailService.addVerificationCode(null, email, verificationCode); + return new AuthResponse(null, email, verificationCode); + } else { + return new AuthResponse(users.getMe(visitor), null, null); + } + } + throw new HttpForbiddenException(); + } +} diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java b/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java index 1ecfd40db..af3a04ad0 100644 --- a/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java +++ b/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java @@ -47,7 +47,7 @@ public class FormatMessageFilter implements Filter { String toUserString = msg.getTo().getUid() == 0 ? String.format("<span class=\"h-card\"><a class=\"mention u-url\" href=\"%s\" data-user-uri=\"1\">@%s</a></span>", msg.getTo().getUri().toASCIIString(), msg.getTo().getName()) : String.format("<span class=\"h-card\"><a class=\"u-url\" href=\"https://juick.com/%s/\">@%s</a></span>", msg.getTo().getName(), msg.getTo().getName()); String formatString = MessageUtils.replyStartsWithQuote(msg) ? "%s,\n%s" : "%s, %s"; - String msgTxt = msg.getRid() > 0 ? String.format(formatString, toUserString, formattedMessage) + String msgTxt = ((msg.getRid() > 0) && msg.getReplyto() > 0) ? String.format(formatString, toUserString, formattedMessage) : formattedMessage; return new SafeString(msgTxt); } diff --git a/src/main/resources/templates/views/thread.html b/src/main/resources/templates/views/thread.html index ab7512947..bb4977308 100644 --- a/src/main/resources/templates/views/thread.html +++ b/src/main/resources/templates/views/thread.html @@ -6,7 +6,7 @@ <div class="msg-cont"> <div class="msg-header"> <div class="msg-avatar"> - <a href="/{{ msg.user.name }}/"><img src="{{ msg.user.avatar }}" alt="{{ msg.user.name }}"/></a> + <a href="/{{ msg.user.name }}/"><img src="{{ msg.user.avatar }}" alt="{{ msg.user.name }}"></a> </div> <span> <a href="/{{ msg.user.name }}/"><span>{{ msg.user.name }}</span>{% if msg.user.premium %}<span style="color: green;"><i data-icon="ei-star" data-size="s"></i></span>{% endif %}</a> @@ -35,7 +35,7 @@ {% if msg.AttachmentType is not empty %} <div class="msg-media"> <a href="//i.juick.com/p/{{ msg.mid }}.{{ msg.AttachmentType }}" data-fname="{{ msg.mid }}.{{ msg.AttachmentType }}"> - <img src="//i.juick.com/photos-512/{{ msg.mid }}.{{ msg.AttachmentType }}" alt=""/> + <img src="//i.juick.com/photos-512/{{ msg.mid }}.{{ msg.AttachmentType }}" alt=""> </a> </div> {% endif %} @@ -102,7 +102,7 @@ </nav> {% if msg.VisitorCanComment %} <form class="msg-comment-target"> - <input type="hidden" name="mid" value="{{ msg.mid }}"/> + <input type="hidden" name="mid" value="{{ msg.mid }}"> <div class="msg-comment"> <div class="ta-wrapper"> <textarea name="body" rows="1" class="reply" placeholder="{{ i18n("messages","message.writeComment") }}"></textarea> @@ -124,11 +124,10 @@ </div> </li> </ul> -<div class="title2"> +<div style="display:none;"> {% if visitor.uid > 0 %} - <img src="/api/thread/mark_read/{{ msg.mid }}-{{ msg.rid }}.gif?hash={{visitor.authHash}}" /> + <img src="/api/thread/mark_read/{{ msg.mid }}-{{ msg.rid }}.gif"> {% endif %} - <h2>{{ i18n("messages","reply.replies") }} ({{ replies.size() }})</h2> </div> <ul id="replies"> @@ -140,13 +139,13 @@ <a class="a-username" href="/{{ msg.user.name }}/">{{ msg.user.name }}{% if msg.user.premium %}<span style="color: green;"><i data-icon="ei-star" data-size="s"></i></span>{% endif %}</a> <div class="msg-avatar"> <a class="a-username" href="/{{ msg.user.name }}/"> - <img src="{{ msg.user.avatar }}" alt="{{ msg.user.name }}"/> + <img src="{{ msg.user.avatar }}" alt="{{ msg.user.name }}"> </a> </div> {% else %} [удалено]: <div class="msg-avatar"> - <img src="//i.juick.com/av-96.png"/> + <img src="//i.juick.com/av-96.png"> </div> {% endif %} <div class="msg-ts"> @@ -165,7 +164,7 @@ {% if msg.AttachmentType is not empty %} <div class="msg-media"> <a href="//i.juick.com/p/{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}" data-fname="{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}"> - <img src="//i.juick.com/photos-512/{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}" alt=""/> + <img src="//i.juick.com/photos-512/{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}" alt=""> </a> </div> {% endif %} |