Friends page, Profile page, Settings, Images

This commit is contained in:
Michael Wain 2024-02-25 20:08:38 +03:00
parent c19bba5930
commit 064ded5fe5
16 changed files with 391 additions and 22 deletions

View File

@ -147,6 +147,15 @@ public class APIController {
return ResponseEntity.badRequest().build();
}
@PostMapping("/api/v1/friends/remove/")
public ResponseEntity<String> removeFriend( @RequestParam("friend_id") Long friend_id ) {
if( userService.findById(friend_id) == null ) return ResponseEntity.badRequest().build();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = userService.findByUsername(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername()).getId();
friendService.removeFriend(userId, friend_id);
return ResponseEntity.ok().build();
}
@PostMapping("/async/notify/get/")
@ResponseBody
public DeferredResult<LongPollResult> getNotify(@RequestParam("last_chat_id") Long last_chat_id,

View File

@ -1,14 +1,21 @@
package com.alterdekim.game.controller;
import com.alterdekim.game.dto.FriendPageResult;
import com.alterdekim.game.dto.UserDTO;
import com.alterdekim.game.entities.Invite;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.InviteService;
import com.alterdekim.game.service.InviteServiceImpl;
import com.alterdekim.game.service.UserService;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.AuthenticationUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -23,27 +30,22 @@ public class AuthController {
private final String base_title = " | Nosedive";
private final UserService userService;
private final InviteService inviteService;
@Autowired
private UserServiceImpl userService;
public AuthController(UserService userService, InviteService inviteService) {
this.inviteService = inviteService;
this.userService = userService;
}
@GetMapping("/game")
public String gamePage(Model model) {
return "game";
}
@Autowired
private InviteServiceImpl inviteService;
@GetMapping("/login")
public String loginPage(Model model) {
AuthenticationUtil.authProfile(model, userService);
model.addAttribute("title", "Login" + base_title);
return "login";
}
@GetMapping("/signup")
public String showRegistrationForm(Model model) {
AuthenticationUtil.authProfile(model, userService);
UserDTO userDto = new UserDTO();
model.addAttribute("user", userDto);
return "signup";

View File

@ -1,8 +1,11 @@
package com.alterdekim.game.controller;
import com.alterdekim.game.dto.AuthApiObject;
import com.alterdekim.game.dto.FriendPageResult;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.FriendServiceImpl;
import com.alterdekim.game.service.UserServiceImpl;
import com.alterdekim.game.util.AuthenticationUtil;
import com.alterdekim.game.util.Hash;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -19,32 +22,56 @@ public class StaticController {
@Autowired
private UserServiceImpl userService;
@Autowired
private FriendServiceImpl friendService;
@GetMapping("/rules")
public String rulesPage(Model model) {
AuthenticationUtil.authProfile(model, userService);
return "rules";
}
@GetMapping("/game")
public String gamePage(Model model) {
return "game";
}
@GetMapping("/games")
public String gamesPage(Model model) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User u = userService.findByUsername(((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername());
Long userId = u.getId();
User u = AuthenticationUtil.authProfile(model, userService);
String apiKey = Hash.sha256((u.getId() + u.getUsername() + u.getPassword()).getBytes());
model.addAttribute("auth_obj", new AuthApiObject(apiKey, userId, Hash.rnd()));
model.addAttribute("auth_obj", new AuthApiObject(apiKey, u.getId(), Hash.rnd()));
} catch ( Exception e ) {
log.error(e.getMessage(), e);
}
return "games";
}
@GetMapping("/friends")
public String friendsPage(Model model) {
Long userId = AuthenticationUtil.authProfile(model, userService).getId();
model.addAttribute("friends", friendService.getFriendsOfUserId(userId).stream()
.map(l -> userService.findById(l))
.map(u -> new FriendPageResult("/profile/"+u.getId(), "background-image: url(&quot;https://i.dogecdn.wtf/wxEpEiovduvcXadQ&quot;);", u.getUsername(), u.getId())));
return "friends";
}
@GetMapping("/settings")
public String settingsPage(Model model) {
Long userId = AuthenticationUtil.authProfile(model, userService).getId();
return "settings";
}
@GetMapping("/")
public String homePage(Model model) {
AuthenticationUtil.authProfile(model, userService);
return "index";
}
@GetMapping("/access-denied")
public String accessDenied(Model model) {
AuthenticationUtil.authProfile(model, userService);
model.addAttribute("title", "Access denied");
return "access-denied";
}

View File

@ -0,0 +1,15 @@
package com.alterdekim.game.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class FriendPageResult {
private String href;
private String avatar;
private String username;
private Long id;
}

View File

@ -3,9 +3,11 @@ package com.alterdekim.game.repository;
import com.alterdekim.game.entities.FriendStatus;
import com.alterdekim.game.entities.Room;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@ -13,4 +15,9 @@ import java.util.List;
public interface FriendRepository extends JpaRepository<FriendStatus, Long> {
@Query(value = "SELECT IF(f.firstUserId = :userId, f.secondUserId, f.firstUserId) FROM FriendStatus f WHERE (f.firstUserId = :userId OR f.secondUserId = :userId) AND f.status = 2")
List<Long> getFriendsOfUserId(@Param("userId") Long userId);
@Transactional
@Modifying
@Query(value = "DELETE FROM FriendStatus f WHERE ((f.firstUserId = :userId AND f.secondUserId = :friendId) OR (f.firstUserId = :friendId AND f.secondUserId = :userId)) AND f.status = 2")
void removeFriend(@Param("userId") Long userId, @Param("friendId") Long friendId);
}

View File

@ -39,6 +39,8 @@ public class SpringSecurity {
.requestMatchers("/games").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/profile/**").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/api/**").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/friends").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/settings").hasAnyAuthority("ROLE_ADMIN")
.requestMatchers("/static/**").permitAll()
.requestMatchers("/access-denied").permitAll()
.requestMatchers("/signup").permitAll()

View File

@ -14,4 +14,8 @@ public class FriendServiceImpl {
public List<Long> getFriendsOfUserId(Long userId) {
return repository.getFriendsOfUserId(userId);
}
public void removeFriend(Long userId, Long friendId) {
repository.removeFriend(userId, friendId);
}
}

View File

@ -0,0 +1,26 @@
package com.alterdekim.game.util;
import com.alterdekim.game.dto.FriendPageResult;
import com.alterdekim.game.entities.User;
import com.alterdekim.game.service.UserServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.ui.Model;
@Slf4j
public class AuthenticationUtil {
public static User authProfile(Model model, UserServiceImpl userService) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if( authentication.isAuthenticated() ) {
try {
User u = userService.findByUsername(((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername());
model.addAttribute("profile", new FriendPageResult("/profile/" + u.getId(), "", u.getUsername(), u.getId()));
return u;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
return null;
}
}

View File

@ -0,0 +1,60 @@
.block {
display: block;
border-bottom: 2px solid var(--dive-shadow-second);
border-radius: 5px;
padding-top: .1px;
padding-bottom: .1px;
width: 100%;
background-color: var(--dive-white);
max-width: 1000px;
min-height: 30vh;
}
.block-content {
justify-content: center;
display: flex;
margin-bottom: 15px;
}
h2 {
margin-top: 0.5rem;
margin-left: 0.5rem;
}
.friends-list-one-info-actions > button {
margin-top: 5px;
}
.friends-list {
display: inline-block;
}
.friends-list-one {
float: left;
width: 320px;
height: 95px;
padding-top: 10px;
padding-bottom: 10px;
}
.friends-list-one-avatar {
float: left;
width: 75px;
height: 75px;
border-radius: 3px;
background-size: cover;
background-position: center;
}
.friends-list-one-info {
float: left;
padding-left: 15px;
width: 240px;
}
.friends-list-one-info-nick {
height: 25px;
font-weight: 300;
font-size: 21px;
line-height: 25px;
}

View File

@ -0,0 +1,69 @@
.container {
width: 1000px;
}
.grid {
display: grid !important;
grid-template-columns: repeat(12,1fr);
grid-column-gap: 25px;
}
.g-col-8 {
grid-column-end: span 8;
min-width: 0;
}
.g-col-4 {
grid-column-end: span 4;
min-width: 0;
}
.block {
display: block;
border-bottom: 2px solid var(--dive-shadow-second);
border-radius: 5px;
padding-top: 0.1px;
padding-bottom: 0.1px;
width: 100%;
min-height: 1px;
background-color: var(--dive-white);
}
.title {
margin-bottom: 15px;
font-size: 18px;
margin-top: 20px;
margin-right: 20px;
margin-left: 20px;
}
.block-content {
justify-content: center;
display: flex;
margin-bottom: 15px;
}
.text-glowing {
color: var(--dive-primary-color) !important;
}
.profile-img {
width: 6.9rem;
height: 6.9rem;
border-radius: 50%;
}
.menu-item {
color: var(--dive-dark-grey-second);
display: flex;
align-items: center;
padding: 5px 20px;
height: 38px;
cursor: pointer;
font-size: 1.1rem;
text-decoration: none;
}
.menu-item._selected {
background-color: var(--dive-primary-color-trans);
}

View File

@ -0,0 +1,20 @@
$(document).ready(function() {
$(".friends-list-one").hover(function() {
$(this).find(".friends-list-one-info").find(".friends-list-one-info-actions").css("display", "");
}, function() {
$(this).find(".friends-list-one-info").find(".friends-list-one-info-actions").css("display", "none");
});
});
function removeFriend(obj) {
let friend_id = $(obj).attr("data-friend-id");
$.ajax({
url: "/api/v1/friends/remove/",
method: "POST",
data: {
friend_id: friend_id
}
}).done(function() {
$(obj).parent().parent().parent().remove();
});
}

View File

@ -151,7 +151,7 @@ function successPolling(data) {
}
}
fids.push({id: friend.id, username: friend.username});
let fr_html = '<div class="friend-one" data-friend-id="'+friend.id+'"><a href="/profile/'+friend.id+'" class="navbar-btn"><img class="navbar-profile-img" src="https://avatars.githubusercontent.com/u/102559365?v=4"></a><span>'+friend.username+'</span><ion-icon onClick="sendInviteMessage('+friend.id+')" name="person-add-outline" role="img" class="md hydrated"></ion-icon></div>';
let fr_html = '<div class="friend-one" data-friend-id="'+friend.id+'"><a href="/profile/'+friend.id+'" class="navbar-btn"><img class="navbar-profile-img" src="https://avatars.githubusercontent.com/u/102559365?v=4"></a><span>'+friend.username+'</span><ion-icon onClick="sendInviteMessage('+friend.id+')" name="person-add" role="img" class="md hydrated"></ion-icon></div>';
$(".friends-online-list").append(fr_html);
} else if( friend.action == 'REMOVE' ) {
for( let u = 0; u < fids.length; u++ ) {

View File

@ -30,13 +30,13 @@
<li class="nav-item">
<div class="dropdown">
<a class="navbar-btn dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
<img class="navbar-profile-img" src="https://avatars.githubusercontent.com/u/102559365?v=4">
<img class="navbar-profile-img" th:src="${profile.avatar}">
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item">username</a></li>
<li><a class="dropdown-item" th:text="${profile.username}"></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/profile/">Profile</a></li>
<li><a class="dropdown-item" href="/settings/">Settings</a></li>
<li><a class="dropdown-item" th:href="${profile.href}">Profile</a></li>
<li><a class="dropdown-item" href="/settings">Settings</a></li>
<li><a class="dropdown-item" href="/logout">Logout</a></li>
</ul>
</div>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<th:block th:insert="~{fragments/head}"></th:block>
<link rel="stylesheet" href="/static/css/friends.css"/>
</head>
<body>
<th:block th:insert="~{fragments/navbar}"></th:block>
<div class="container mt-5 block">
<h2>Friends</h2>
<div class="friends-list">
<th:block th:each="friend: ${friends}">
<div class="friends-list-one">
<a th:href="${friend.href}">
<div class="friends-list-one-avatar" th:style="${friend.avatar}"></div>
</a>
<div class="friends-list-one-info">
<div class="friends-list-one-info-nick">
<a th:href="${friend.href}" th:text="${friend.username}"></a>
</div>
<div class="friends-list-one-info-actions" style="display: none;">
<button onClick="removeFriend(this)" class="btn btn-danger btn-sm" th:data-friend-id="${friend.id}">Remove</button>
</div>
</div>
</div>
</th:block>
</div>
</div>
<th:block th:insert="~{fragments/essentials}"></th:block>
<script src="/static/javascript/friends.js"></script>
</body>
</html>

View File

@ -27,7 +27,7 @@
<div class="game">
<div class="players">
<th:block th:each="p: ${players}">
<div class="player" data-pid="${p.id}" onClick="drop(this)">
<div class="player" th:data-pid="${p.id}" onClick="drop(this)">
<p class="timeout"></p>
<p class="nickname" th:text="${p.name}"></p>
<p class="money" th:text="${p.money}"></p>
@ -215,7 +215,7 @@
<div class="up"><div class="board_field corner"><img src="../static/images/start.png" style="width: 100%; height: 100%;" /></div></div>
<th:block th:each="f: ${fields_up}">
<div th:data-fid="${f.uid}" class="board_field" style="grid-column: @{${f.id}}"> <!-- 2 + -->
<div th:data-fid="${f.uid}" class="board_field" th:style="grid-column: @{${f.id}}"> <!-- 2 + -->
<div class="cost" th:text="${f.cost}"></div>
<div class="fh">
<div class="iconH">

View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<th:block th:insert="~{fragments/head}"></th:block>
<link rel="stylesheet" href="/static/css/settings.css"/>
</head>
<body>
<th:block th:insert="~{fragments/navbar}"></th:block>
<div class="container">
<div class="grid mt-5">
<div class="g-col-8">
<div class="block" id="profile" style="margin-top: 15px;">
<div class="title">Profile</div>
<div class="block-content">
<div class="grid">
<div class="g-col-4" style="display: flex; justify-content: center;">
<div style="width: 50%;">
<img class="profile-img" src="https://tg.dogecdn.wtf/v2/AgACAgIAAx0EWSIHJgAC0MBf64kG8Q99Gm69NezdtLUxTJaBiQACvrExG1sQWEtraYhjusGQAAGJdBmYLgADAQADAgADeAAD8RcFAAEeBA.jpg">
<button class="btn btn-primary" style="margin-top: 10px; font-size: 0.7rem;">Update avatar</button>
</div>
</div>
<div class="g-col-8" style="padding: 5px;">
<div class="mb-3">
<label for="displayname-input" class="form-label">Display name (may contain unicode symbols)</label>
<input type="text" class="form-control" id="displayname-input" autocomplete="off">
</div>
<div class="mb-3">
<label for="nickname-input" class="form-label">Nickname</label>
<input type="text" class="form-control" id="nickname-input" autocomplete="off">
</div>
<div class="mb-3">
<label for="pronouns-select" class="form-label">Pronouns</label>
<select class="form-select" id="pronouns-select" aria-label="">
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="block" id="security" style="margin-top: 15px;">
<div class="title">Security</div>
<div class="block-content">
<div class="grid">
<div class="g-col-4" style="display: flex; justify-content: center;">
</div>
<div class="g-col-8" style="padding: 5px;">
</div>
</div>
</div>
</div>
<div class="block" id="privacy" style="margin-top: 15px;">
<div class="title">Privacy</div>
<div class="block-content">
<div class="grid">
<div class="g-col-4" style="display: flex; justify-content: center;">
</div>
<div class="g-col-8" style="padding: 5px;">
</div>
</div>
</div>
</div>
</div>
<div class="g-col-4">
<div class="block" style="margin-top: 15px;">
<div class="title">Settings</div>
<div class="block-content" style="display: block;">
<a href="#profile" class="menu-item _selected">Profile</a>
<a href="#security" class="menu-item">Security</a>
<a href="#privacy" class="menu-item">Privacy</a>
</div>
</div>
<div class="block" style="margin-top: 15px;">
<div class="title">Data</div>
<div class="block-content" style="display: block;">
<a href="#blacklist" class="menu-item">Black list</a>
<a href="#notes" class="menu-item">Notes</a>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/essentials}"></th:block>
</body>
</html>