Admin panel user properties editing added

This commit is contained in:
Michael Wain 2025-03-08 19:54:49 +03:00
parent ab080773c8
commit dd2fafd05f
18 changed files with 333 additions and 66 deletions

View File

@ -169,6 +169,10 @@ public class GameServer {
} }
this.sendResult(playerId, message.getTransactionId(), xmlMapper.writeValueAsString(locationService.getLocationById(nextLocation)).replace("&lt;", "<")); this.sendResult(playerId, message.getTransactionId(), xmlMapper.writeValueAsString(locationService.getLocationById(nextLocation)).replace("&lt;", "<"));
} }
case GetCatalogInfo -> {
long catalogObjectId = message.getArguments().get(0).getLong();
// todo: implement
}
case ChatMessage -> { case ChatMessage -> {
String text = message.getArguments().get(1).toString(); String text = message.getArguments().get(1).toString();
sendInPlayersLocation(playerId, CommandType.ChatMessage, new ChatMessage(playerId, text)); sendInPlayersLocation(playerId, CommandType.ChatMessage, new ChatMessage(playerId, text));

View File

@ -141,7 +141,6 @@ public class StartUpListener {
public void onApplicationEvent(ContextRefreshedEvent event) { public void onApplicationEvent(ContextRefreshedEvent event) {
this.startTime = System.currentTimeMillis(); this.startTime = System.currentTimeMillis();
// todo: compile other swf's
if( userService.findByUsername(ADMIN_USERNAME) != null ) { if( userService.findByUsername(ADMIN_USERNAME) != null ) {
// todo: remove hardcoded cache folder // todo: remove hardcoded cache folder
@ -161,7 +160,6 @@ public class StartUpListener {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
// todo: compile base swf with some changes (make smiles work)
if( userService.findByUsername(ADMIN_USERNAME) != null ) return; if( userService.findByUsername(ADMIN_USERNAME) != null ) return;
@ -173,8 +171,6 @@ public class StartUpListener {
try { try {
processResources(storageProperties.getLocation() + File.separator + config.getDefaultResourcesPath()); processResources(storageProperties.getLocation() + File.separator + config.getDefaultResourcesPath());
processBase(storageProperties.getLocation() + File.separator + config.getDefaultBasePath()); processBase(storageProperties.getLocation() + File.separator + config.getDefaultBasePath());
// todo: implements miniquests
String miniquests = storageProperties.getLocation() + File.separator + config.getDefaultMiniquestsPath();
processCatalogs(storageProperties.getLocation() + File.separator + config.getDefaultCatalogsPath()); processCatalogs(storageProperties.getLocation() + File.separator + config.getDefaultCatalogsPath());
} catch (IOException e) { } catch (IOException e) {
log.error("Unable to move pre-compiled data to the server's database: {}", e.getMessage()); log.error("Unable to move pre-compiled data to the server's database: {}", e.getMessage());

View File

@ -14,6 +14,7 @@ public enum UserCommandType {
UpdateUserData("_D"), UpdateUserData("_D"),
GetUserLocation("_LG"), GetUserLocation("_LG"),
GetUserLocationById("_LGI"), GetUserLocationById("_LGI"),
GetCatalogInfo("_GCI"),
ChatMessage("_C"), ChatMessage("_C"),
Shoot("_NUS"), Shoot("_NUS"),
GetUserInfo("_UI"), GetUserInfo("_UI"),

View File

@ -3,6 +3,10 @@ package com.alterdekim.game.controller;
import com.alterdekim.game.component.GameServer; import com.alterdekim.game.component.GameServer;
import com.alterdekim.game.component.NativeDao; import com.alterdekim.game.component.NativeDao;
import com.alterdekim.game.component.StartUpListener; import com.alterdekim.game.component.StartUpListener;
import com.alterdekim.game.component.game.ClubAccessType;
import com.alterdekim.game.component.game.PlayerProperties;
import com.alterdekim.game.component.game.PlayerPropertyType;
import com.alterdekim.game.component.game.RoleFlags;
import com.alterdekim.game.controller.result.api.ApiResult; import com.alterdekim.game.controller.result.api.ApiResult;
import com.alterdekim.game.controller.result.api.PreloaderResult; import com.alterdekim.game.controller.result.api.PreloaderResult;
import com.alterdekim.game.controller.result.api.PromotionBannerResult; import com.alterdekim.game.controller.result.api.PromotionBannerResult;
@ -103,7 +107,8 @@ public class ApiController {
Promotions("promotions", Promotion.class, StaticController.PanelSection.Preloaders), Promotions("promotions", Promotion.class, StaticController.PanelSection.Preloaders),
Banners("promotion_banners", PromotionBanner.class, StaticController.PanelSection.Preloaders), Banners("promotion_banners", PromotionBanner.class, StaticController.PanelSection.Preloaders),
Locations("locations", Location.class, StaticController.PanelSection.Locations), Locations("locations", Location.class, StaticController.PanelSection.Locations),
LocationObjects("location_objects",LocationObjectInstance .class, StaticController.PanelSection.Locations); LocationObjects("location_objects",LocationObjectInstance .class, StaticController.PanelSection.Locations),
Users("users", User.class, StaticController.PanelSection.Users);
private final String val; private final String val;
private final Class<?> cl; private final Class<?> cl;
@ -151,6 +156,7 @@ public class ApiController {
case Banners -> promotionBannerService.getAll(); case Banners -> promotionBannerService.getAll();
case Locations -> locationService.getAll(); case Locations -> locationService.getAll();
case LocationObjects -> locationService.getAllLocationObjects(); case LocationObjects -> locationService.getAllLocationObjects();
case Users -> userService.getAll();
}; };
return ResponseEntity.ok( return ResponseEntity.ok(
@ -161,6 +167,54 @@ public class ApiController {
); );
} }
@RequestMapping(value = "/reset_user_property", method = RequestMethod.POST)
public ResponseEntity resetUserProperty(@RequestParam("user_id") Integer userId, @RequestParam("entry_name") String entryName) {
PlayerProperties property = PlayerProperties.valueOf(entryName);
userService.removeUserProperty(userId, property);
userService.getUserInitInfoByUserId(userId);
return ResponseEntity.ok().build();
}
@RequestMapping(value = "/edit_user_property", method = RequestMethod.POST)
public ResponseEntity editUserProperty(@RequestParam("user_id") Integer userId,
@RequestParam("entry_name") String entryName,
@RequestParam("entry_value") String entryValue) {
PlayerProperties property = PlayerProperties.valueOf(entryName);
Object val = switch (property.getValueType()) {
case Enum -> {
if( property == PlayerProperties.RoleFlags ) {
yield RoleFlags.valueOf(entryValue);
}
yield ClubAccessType.valueOf(entryValue);
}
case Boolean -> Boolean.parseBoolean(entryValue);
default -> Integer.parseInt(entryValue);
};
userService.pushUserProperty(userId, property, val);
return ResponseEntity.ok().build();
}
@RequestMapping(value = "/get_user_properties", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<UserPropertyResult>> getUserProperties(@RequestParam("id") Integer userId) {
List<UserPropertyResult> results = new LinkedList<>();
for( PlayerProperties property : PlayerProperties.values() ) {
Object value = userService.findUserProperty(userId, property).orElse(null);
if( value == null ) continue;
results.add(new UserPropertyResult(property.getValueType(), property.name(), value));
}
return ResponseEntity.ok(results);
}
@AllArgsConstructor
public static class UserPropertyResult {
@JsonProperty
private PlayerPropertyType type;
@JsonProperty
private String name;
@JsonProperty
private Object value;
}
@RequestMapping(value = "/add_row", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/add_row", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<AddEntryResponse> addSpecificRow(HttpServletRequest httpServletRequest) { public ResponseEntity<AddEntryResponse> addSpecificRow(HttpServletRequest httpServletRequest) {
if( !httpServletRequest.getParameterMap().containsKey("table") ) return ResponseEntity.badRequest().build(); if( !httpServletRequest.getParameterMap().containsKey("table") ) return ResponseEntity.badRequest().build();

View File

@ -68,6 +68,13 @@ public class StaticController {
return "panel"; return "panel";
} }
@GetMapping("/panel_user")
public String panelUser(Model model, @RequestParam(value = "id", defaultValue = "1") Integer userId) {
model.addAttribute("user_id", userId);
model.addAttribute("user_name", userService.getUsernameById(userId).get());
return "panel_user";
}
public enum PanelSection { public enum PanelSection {
Dashboard, Dashboard,
Locations, Locations,

View File

@ -0,0 +1,16 @@
package com.alterdekim.game.controller.result.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class UserResult {
@JsonProperty
private Integer id;
@JsonProperty
private String username;
@JsonProperty
private Boolean isBanned;
}

View File

@ -2,6 +2,8 @@ package com.alterdekim.game.entity;
import com.alterdekim.game.component.game.ClubAccessType; import com.alterdekim.game.component.game.ClubAccessType;
import com.alterdekim.game.component.game.RoleFlags; import com.alterdekim.game.component.game.RoleFlags;
import com.alterdekim.game.controller.result.api.ApiResult;
import com.alterdekim.game.controller.result.api.UserResult;
import com.alterdekim.game.repository.UserRepository; import com.alterdekim.game.repository.UserRepository;
import com.alterdekim.game.xml.NumericBooleanSerializer; import com.alterdekim.game.xml.NumericBooleanSerializer;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -26,7 +28,7 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
@Entity @Entity
@Table(name="users") @Table(name="users")
public class User { public class User implements ApiResult {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@ -53,4 +55,9 @@ public class User {
this.id = id; this.id = id;
this.username = username; this.username = username;
} }
@Override
public UserResult toAPIResult() {
return new UserResult(this.id, this.username, this.isBanned);
}
} }

View File

@ -37,6 +37,7 @@ public class SpringSecurity {
.requestMatchers("/").permitAll() .requestMatchers("/").permitAll()
.requestMatchers("/login").permitAll() .requestMatchers("/login").permitAll()
.requestMatchers("/panel").hasAuthority(Role.RoleType.RoleAdmin.name()) .requestMatchers("/panel").hasAuthority(Role.RoleType.RoleAdmin.name())
.requestMatchers("/panel_user").hasAuthority(Role.RoleType.RoleAdmin.name())
.requestMatchers("/api/**").hasAuthority(Role.RoleType.RoleAdmin.name()) .requestMatchers("/api/**").hasAuthority(Role.RoleType.RoleAdmin.name())
.requestMatchers("/main").hasAnyAuthority(Role.RoleType.RoleUser.name(), Role.RoleType.RoleAdmin.name()) .requestMatchers("/main").hasAnyAuthority(Role.RoleType.RoleUser.name(), Role.RoleType.RoleAdmin.name())
.requestMatchers("/"+ FileServerController.URL_PATH +"/**").permitAll() .requestMatchers("/"+ FileServerController.URL_PATH +"/**").permitAll()

View File

@ -172,7 +172,7 @@ public class AvatarInventoryService {
} }
public List<InitMagicAbility> getMagicAbilitiesItemsByUserId(Integer userId) { public List<InitMagicAbility> getMagicAbilitiesItemsByUserId(Integer userId) {
return this.inventoryRepository.findMagicAbilitiesByUserId(userId); // todo: implement expiration ability return this.inventoryRepository.findMagicAbilitiesByUserId(userId);
} }
public List<Smile> getSmilesByUserId(Integer userId) { public List<Smile> getSmilesByUserId(Integer userId) {

View File

@ -58,6 +58,10 @@ public class UserService {
return userRepository.findByUsername(username); return userRepository.findByUsername(username);
} }
public List<User> getAll() {
return this.userRepository.findAll();
}
public int saveUser(String username, String password, Role.RoleType role) { public int saveUser(String username, String password, Role.RoleType role) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
@ -87,6 +91,10 @@ public class UserService {
}); });
} }
public void removeUserProperty(Integer userId, PlayerProperties type) {
this.propertyRepository.deleteByUserIdAndType(userId, type);
}
public void pushUserProperty(Integer userId, PlayerProperties type, Object val) { public void pushUserProperty(Integer userId, PlayerProperties type, Object val) {
this.propertyRepository.deleteByUserIdAndType(userId, type); this.propertyRepository.deleteByUserIdAndType(userId, type);
this.propertyRepository.save(switch (type.getValueType()) { this.propertyRepository.save(switch (type.getValueType()) {

View File

@ -0,0 +1,118 @@
const roleFlags = [
"NONE",
"MEMBER",
"SA",
"MODERATOR",
"ADMINISTRATOR",
"CHIEF",
"HIDDEN"
];
const clubAccessType = [
"OPEN",
"CLOSED",
"FRIENDS_ONLY",
];
function initPropertyTable(tag, data) {
if( data.length <= 0 ) return;
let keys = Object.keys(data[0]);
let header = "";
for( let i = 1; i < keys.length; i++ ) {
header += "<th>" + keys[i] + "</th>";
}
header += "<th>actions</th>";
$($(tag).children("thead")[0]).html("<tr>"+header+"</tr>");
for( let i = 0; i < data.length; i++ ) {
header = "";
let entry_type = "";
let entry_name = "";
for( let u = 0; u < keys.length; u++ ) {
if( u == 0 ) {
entry_type = data[i][keys[u]];
header += "<td class=\"entry-type\" style=\"display: none;\">"+data[i][keys[u]]+"</td>";
continue;
} else if( u == 1 ) {
entry_name = data[i][keys[u]];
} else if( u == 2 ) {
switch(entry_type) {
case "Enum":
header += "<td data-name=\""+keys[u]+"\"><select>";
let current_val = data[i][keys[u]];
if( entry_name == "RoleFlags" ) {
header += "<option value=\""+current_val+"\">"+current_val+"</option>";
for( let j = 0; j < roleFlags.length; j++ ) {
if( roleFlags[j] == current_val ) {
continue;
}
header += "<option value=\""+roleFlags[j]+"\">"+roleFlags[j]+"</option>";
}
} else if( entry_name == "ClubAccessType" ) {
header += "<option value=\""+current_val+"\">"+current_val+"</option>";
for( let j = 0; j < clubAccessType.length; j++ ) {
if( clubAccessType[j] == current_val ) {
continue;
}
header += "<option value=\""+clubAccessType[j]+"\">"+clubAccessType[j]+"</option>";
}
}
header += "</td></select>";
break;
case "Boolean":
let v = "";
if( data[i][keys[u]]+"" == "true" ) {
v = "checked";
}
header += "<td data-name=\""+keys[u]+"\"><input type=\"checkbox\"" + v + "/></td>";
break;
default: // Integer
header += "<td data-name=\""+keys[u]+"\"><input type=\"number\" value=\""+data[i][keys[u]]+"\"/></td>";
}
continue;
}
header += "<td data-name=\""+keys[u]+"\">" + data[i][keys[u]] + "</td>";
}
header += "<td><img src=\"/file/svg/rotate.svg\" onclick=\"resetRow(this)\"></td>";
header += "<td><img src=\"/file/svg/pencil.svg\" onclick=\"editRow(this)\"></td>";
let tabName = tag.substring(1);
$($(tag).children("tbody")[0]).append("<tr data-table=\""+tabName[0].toUpperCase()+tabName.substring(1)+"\">"+header+"</tr>");
}
}
function editRow(obj) {
let entry_name = $(obj).parent().parent().find("td[data-name='name']").html();
let entry_value = $(obj).parent().parent().find("td[data-name='value']");
let entry_type = $(obj).parent().parent().find(".entry-type").html();
if( entry_type == "Enum") {
entry_value = entry_value.find("select").find(":selected").val();
} else if( entry_type == "Boolean" ) {
entry_value = entry_value.find("input").is(":checked");
} else {
entry_value = entry_value.find("input").val();
}
console.log(entry_name, entry_type, entry_value);
$.post( "/api/edit_user_property?user_id="+$("#user_init_info").attr("data-id")+"&entry_name="+entry_name+"&entry_value="+entry_value, function() {
window.location.reload();
});
}
function resetRow(obj) {
let entry_name = $(obj).parent().parent().find("td[data-name='name']").html();
$.post( "/api/reset_user_property?user_id="+$("#user_init_info").attr("data-id")+"&entry_name="+entry_name, function() {
window.location.reload();
});
}
function set_loading(obj, state) {
$(obj).attr("aria-busy", state);
}
function getUserProperties() {
$.get( "/api/get_user_properties?id="+$("#user_init_info").attr("data-id"), function( data ) {
initPropertyTable("#properties", data);
set_loading("#properties", false);
});
}
getUserProperties();

View File

@ -0,0 +1,18 @@
function updateUsersTable() {
let rows = $("tbody").find("tr");
for( let i = 0; i < rows.length; i++ ) {
let row = rows[i];
let userId = $(row).attr("data-rownum");
$($(row).find("td")[1]).html("<a href=\"/panel_user?id="+userId+"\">"+$($(row).find("td")[1]).html()+"</a>");
}
}
function getUsers() {
$.get( "/api/get_entity_rows?entity=Users", function( data ) {
initTable("#users", data);
updateUsersTable();
set_loading("#users", false);
});
}
getUsers();

View File

@ -0,0 +1,20 @@
aside > nav > ul > li {
font-size: 18px;
}
.del-btn {
padding: 0.3rem 0.5rem;
}
.add-btn {
margin-left: 0.2rem;
cursor: pointer;
width: 24px;
height: 24px;
}
td > img {
width: 28px;
height: 28px;
cursor: pointer;
}

View File

@ -0,0 +1,20 @@
<th:block th:fragment="aside">
<aside>
<nav>
<ul>
<li><a href="/panel">Dashboard</a></li>
<li><a href="/panel?section=Locations">Locations</a></li>
<li><a href="/panel?section=Games">Games</a></li>
<li><a href="/panel?section=Preloaders">Preloaders</a></li>
<li><a href="/panel?section=Users">Users</a></li>
<li><a href="/panel?section=Banlist">Banlist</a></li>
<li><a href="/panel?section=Cache">Cache</a></li>
<li><a href="/panel?section=Goods">Goods</a></li>
<li><a href="/panel?section=Items">Items list</a></li>
<li><a href="/panel?section=Actions">Actions</a></li>
<li><a href="/panel?section=Support">Support</a></li>
<li><a href="/panel?section=Settings">Settings</a></li>
</ul>
</nav>
</aside>
</th:block>

View File

@ -0,0 +1,9 @@
<th:block th:fragment="head">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="/file/css/pico.min.css">
<script src="/file/_js/jquery-3.7.1.min.js"></script>
<title>WhimsyWorld</title>
<link rel="stylesheet" href="/file/css/panel.css">
</th:block>

View File

@ -0,0 +1,15 @@
<th:block th:fragment="header">
<header>
<div class="container">
<nav>
<ul>
<li><strong>WhimsyWorld</strong></li>
</ul>
<ul>
<li><a href="https://gitea.awain.net/alterwain/WhimsyWorld"><img src="/file/svg/github.svg"/></a></li>
<li><a href="/logout"><button class="secondary">Logout</button></a></li>
</ul>
</nav>
</div>
</header>
</th:block>

View File

@ -1,68 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head> <head>
<meta charset="utf-8"> <th:block th:insert="~{fragments/head}"></th:block>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="/file/css/pico.min.css">
<script src="/file/_js/jquery-3.7.1.min.js"></script>
<title>WhimsyWorld</title>
<style>
aside > nav > ul > li {
font-size: 18px;
}
.del-btn {
padding: 0.3rem 0.5rem;
}
.add-btn {
margin-left: 0.2rem;
cursor: pointer;
width: 24px;
height: 24px;
}
td > img {
width: 28px;
height: 28px;
cursor: pointer;
}
</style>
</head> </head>
<body> <body>
<header> <th:block th:insert="~{fragments/header}"></th:block>
<div class="container">
<nav>
<ul>
<li><strong>WhimsyWorld</strong></li>
</ul>
<ul>
<li><a href="https://gitea.awain.net/alterwain/WhimsyWorld"><img src="/file/svg/github.svg"/></a></li>
<li><a href="/logout"><button class="secondary">Logout</button></a></li>
</ul>
</nav>
</div>
</header>
<main class="container grid" style="grid-template-columns: 1fr 6fr;"> <main class="container grid" style="grid-template-columns: 1fr 6fr;">
<aside> <th:block th:insert="~{fragments/aside}"></th:block>
<nav>
<ul>
<li><a href="/panel">Dashboard</a></li>
<li><a href="/panel?section=Locations">Locations</a></li>
<li><a href="/panel?section=Games">Games</a></li>
<li><a href="/panel?section=Preloaders">Preloaders</a></li>
<li><a href="/panel?section=Users">Users</a></li>
<li><a href="/panel?section=Banlist">Banlist</a></li>
<li><a href="/panel?section=Cache">Cache</a></li>
<li><a href="/panel?section=Goods">Goods</a></li>
<li><a href="/panel?section=Items">Items list</a></li>
<li><a href="/panel?section=Actions">Actions</a></li>
<li><a href="/panel?section=Support">Support</a></li>
<li><a href="/panel?section=Settings">Settings</a></li>
</ul>
</nav>
</aside>
<div class="container-fluid"> <div class="container-fluid">
<dialog> <dialog>
<article> <article>
@ -119,6 +63,15 @@
</tbody> </tbody>
</table> </table>
</th:block> </th:block>
<th:block th:case="Users">
<h2>Users</h2>
<table id="users" aria-busy="true">
<thead>
</thead>
<tbody>
</tbody>
</table>
</th:block>
<th:block th:case="Add"> <th:block th:case="Add">
<blockquote id="error_response" style="display: none"> <blockquote id="error_response" style="display: none">
An internal server error occurred An internal server error occurred

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<th:block th:insert="~{fragments/head}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/header}"></th:block>
<main class="container grid" style="grid-template-columns: 1fr 6fr;">
<th:block th:insert="~{fragments/aside}"></th:block>
<div class="container-fluid">
<h2 id="user_init_info" th:data-id="${user_id}" th:text="${user_name}"></h2>
<table id="properties" aria-busy="true">
<thead></thead>
<tbody></tbody>
</table>
</div>
</main>
</body>
<script type="text/javascript" src="/file/_js/user.js"></script>
</html>