Initial commit

This commit is contained in:
张宇衡 2022-12-02 15:35:45 +08:00
commit b0aef02e53
108 changed files with 48654 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

114
.gitignore vendored Normal file
View File

@ -0,0 +1,114 @@
# User-specific stuff
.idea/
.vscode/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# Common working directory
run/

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# IdentityVerification
外置登录与实名认证带web后台

98
pom.xml Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>site.deercloud</groupId>
<artifactId>IdentityVerification</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>IdentityVerification</name>
<description>实名认证添加白名单、正版玩家邀请非正版注册管理。</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<url>https://blog.deercloud.site</url>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.13-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.39.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.19</version>
</dependency>
<!--JavaMail基本包-->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<!--邮件发送的扩展包-->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,30 @@
package site.deercloud.identityverification;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import site.deercloud.identityverification.Controller.InviteCodeManager;
import java.util.List;
public class Commands implements TabExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
switch (args[0]) {
case "reload":
IdentityVerification.getInstance().getConfigManager().reload(sender);
case "genCode":
// TODO: 需要校验玩家是否符合活跃度要求
InviteCodeManager.CreateCode(sender);
case "myCodes":
InviteCodeManager.ListCodesOf(sender);
default:
return true;
}
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
return null;
}
}

View File

@ -0,0 +1,101 @@
package site.deercloud.identityverification.Controller;
import site.deercloud.identityverification.HttpServer.model.GameSession;
import site.deercloud.identityverification.IdentityVerification;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public class AFKTracker {
public static final long IGNORES_AFK = -1L;
private final Set<UUID> usedAFKCommand;
private Long afkThresholdMs;
private final IdentityVerification plugin;
public AFKTracker(IdentityVerification plugin) {
this.plugin = plugin;
usedAFKCommand = new HashSet<>();
}
public long getAfkThreshold() {
if (afkThresholdMs == null) {
afkThresholdMs = 1000L * 10L;
}
return afkThresholdMs;
}
public void hasIgnorePermission(UUID playerUUID) {
storeLastMovement(playerUUID, IGNORES_AFK);
}
private void storeLastMovement(UUID playerUUID, long time) {
GameSessionCache.getCacheSession(playerUUID)
.ifPresent(gameSession -> gameSession.setLastMovement(time));
}
private long getLastMovement(UUID playerUUID, long time) {
return getLastMovement(playerUUID)
.orElse(time);
}
private Optional<Long> getLastMovement(UUID playerUUID) {
return GameSessionCache.getCacheSession(playerUUID)
.map(GameSession::getLastMovement);
}
public void usedAfkCommand(UUID playerUUID, long time) {
long lastMoved = getLastMovement(playerUUID, time);
if (lastMoved == IGNORES_AFK) {
return;
}
usedAFKCommand.add(playerUUID);
storeLastMovement(playerUUID, time - getAfkThreshold());
}
public long performedAction(UUID playerUUID, long time) {
long lastMoved = getLastMovement(playerUUID, time);
// Ignore afk permission
if (lastMoved == IGNORES_AFK) {
return 0L;
}
storeLastMovement(playerUUID, time);
try {
if (time - lastMoved < getAfkThreshold()) {
// Threshold not crossed, no action required.
return 0L;
}
long removeAfkCommandEffect = usedAFKCommand.contains(playerUUID) ? getAfkThreshold() : 0;
long timeAFK = time - lastMoved - removeAfkCommandEffect;
GameSessionCache.getCacheSession(playerUUID)
.ifPresent(gameSession -> gameSession.addAfkTime(timeAFK));
return timeAFK;
} finally {
usedAFKCommand.remove(playerUUID);
}
}
public long loggedOut(UUID uuid, long time) {
long timeAFK = performedAction(uuid, time);
usedAFKCommand.remove(uuid);
return timeAFK;
}
public boolean isAfk(UUID playerUUID) {
long time = System.currentTimeMillis();
Optional<Long> lastMoved = getLastMovement(playerUUID);
if (lastMoved.isEmpty() || lastMoved.get() == IGNORES_AFK) {
return false;
}
return time - lastMoved.get() > getAfkThreshold();
}
}

View File

@ -0,0 +1,260 @@
package site.deercloud.identityverification.Controller;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.File;
import java.util.ArrayList;
public class ConfigManager {
public ConfigManager(IdentityVerification plugin) {
this.plugin = plugin;
reload();
}
public void reload() {
plugin.saveDefaultConfig();
plugin.reloadConfig();
config = plugin.getConfig();
m_debug = config.getBoolean("Debug");
m_ServerName = config.getString("Web.ServerName");
m_ImplementationName = config.getString("Web.ImplementationName");
m_ImplementationVersion = config.getString("Web.ImplementationVersion");
m_HomePageUrl = config.getString("Web.HomePageUrl");
m_RegisterUrl = m_HomePageUrl + "/register";
m_WebHost = config.getString("Web.Host");
m_WebPort = config.getInt("Web.Port");
m_YagHost = config.getString("Yggdrasil.Host");
m_YagPort = config.getInt("Yggdrasil.Port");
m_SkinDomains = (ArrayList<String>) config.getStringList("Yggdrasil.SkinDomains");
m_RsaPublicKeyFileName = config.getString("Yggdrasil.RsaPublicKey");
m_RsaPrivateKeyFileName = config.getString("Yggdrasil.RsaPrivateKey");
m_EmailHost = config.getString("Email.Host");
m_EmailPort = config.getString("Email.Port");
m_EmailUsername = config.getString("Email.Username");
m_EmailPassword = config.getString("Email.Password");
m_EmailFrom = config.getString("Email.From");
// 加载配置文件夹下的RSA公钥 私钥
File publicKeyFile = new File(plugin.getDataFolder(), m_RsaPublicKeyFileName);
File privateKeyFile = new File(plugin.getDataFolder(), m_RsaPrivateKeyFileName);
MyLogger.info("配置文件加载完成。");
}
public void reload(CommandSender sender) {
if (sender instanceof Player) {
if (sender.isOp()) {
reload();
MyLogger.info(sender,"配置文件已重新加载。");
} else {
MyLogger.warn(sender,"尝试执行未授权的命令。");
}
} else {
reload();
MyLogger.info("配置文件已重新加载。");
}
}
public String getServerName() {
return m_ServerName;
}
public void setServerName(String serverName) {
m_ServerName = serverName;
config.set("Web.ServerName", serverName);
plugin.saveConfig();
}
public String getImplementationName() {
return m_ImplementationName;
}
public void setImplementationName(String implementationName) {
m_ImplementationName = implementationName;
config.set("Web.ImplementationName", implementationName);
plugin.saveConfig();
}
public String getImplementationVersion() {
return m_ImplementationVersion;
}
public void setImplementationVersion(String implementationVersion) {
m_ImplementationVersion = implementationVersion;
config.set("Web.ImplementationVersion", implementationVersion);
plugin.saveConfig();
}
public String getHomePageUrl() {
return m_HomePageUrl;
}
public void setHomePageUrl(String homePageUrl) {
m_HomePageUrl = homePageUrl;
config.set("Web.HomePageUrl", homePageUrl);
plugin.saveConfig();
}
public String getRegisterUrl() {
return m_RegisterUrl;
}
public String getWebHost() {
return m_WebHost;
}
public void setWebHost(String host) {
m_WebHost = host;
config.set("Web.Host", host);
plugin.saveConfig();
}
public int getWebPort() {
return m_WebPort;
}
public void setWebPort(int port) {
m_WebPort = port;
config.set("Web.Port", port);
plugin.saveConfig();
}
public String getYagHost() {
return m_YagHost;
}
public void setYagHost(String host) {
m_YagHost = host;
config.set("Yggdrasil.Host", host);
plugin.saveConfig();
}
public int getYagPort() {
return m_YagPort;
}
public void setYagPort(int port) {
m_YagPort = port;
config.set("Yggdrasil.Port", port);
plugin.saveConfig();
}
public ArrayList<String> getSkinDomains() {
return m_SkinDomains;
}
public void setSkinDomains(ArrayList<String> skinDomains) {
m_SkinDomains = skinDomains;
config.set("Yggdrasil.SkinDomains", skinDomains);
plugin.saveConfig();
}
public String getSignaturePublicKey() {
return m_SignaturePublicKey;
}
public void setSignaturePublicKey(String signaturePublicKey) {
m_SignaturePublicKey = signaturePublicKey;
config.set("Yggdrasil.SignaturePublicKey", signaturePublicKey);
plugin.saveConfig();
}
public void setPublicKeyFileName(String signaturePublicKey) {
m_RsaPublicKeyFileName = signaturePublicKey;
config.set("Yggdrasil.RsaPublicKey", signaturePublicKey);
plugin.saveConfig();
}
public String getPublicKeyFileName() {
return m_RsaPublicKeyFileName;
}
public String getSignaturePrivateKey() {
return m_SignaturePrivateKey;
}
public void setSignaturePrivateKey(String signaturePrivateKey) {
m_SignaturePrivateKey = signaturePrivateKey;
config.set("Yggdrasil.SignaturePrivateKey", signaturePrivateKey);
plugin.saveConfig();
}
public void setPrivateKeyFileName(String signaturePrivateKey) {
m_RsaPrivateKeyFileName = signaturePrivateKey;
config.set("Yggdrasil.RsaPrivateKey", signaturePrivateKey);
plugin.saveConfig();
}
public String getPrivateKeyFileName() {
return m_RsaPrivateKeyFileName;
}
public Boolean getDebug() {
return m_debug;
}
public void setDebug(Boolean debug) {
m_debug = debug;
config.set("Debug", debug);
plugin.saveConfig();
}
public String getEmailHost() {
return m_EmailHost;
}
public void setEmailHost(String emailHost) {
m_EmailHost = emailHost;
config.set("Email.Host", emailHost);
plugin.saveConfig();
}
public String getEmailPort() {
return m_EmailPort;
}
public void setEmailPort(String emailPort) {
m_EmailPort = emailPort;
config.set("Email.Port", emailPort);
plugin.saveConfig();
}
public String getEmailUsername() {
return m_EmailUsername;
}
public void setEmailUsername(String emailUsername) {
m_EmailUsername = emailUsername;
config.set("Email.Username", emailUsername);
plugin.saveConfig();
}
public String getEmailPassword() {
return m_EmailPassword;
}
public void setEmailPassword(String emailPassword) {
m_EmailPassword = emailPassword;
config.set("Email.Password", emailPassword);
plugin.saveConfig();
}
public String getEmailFrom() {
return m_EmailFrom;
}
public void setEmailFrom(String emailFrom) {
m_EmailFrom = emailFrom;
config.set("Email.From", emailFrom);
plugin.saveConfig();
}
private String m_ServerName;
private String m_ImplementationName;
private String m_ImplementationVersion;
private String m_HomePageUrl;
private String m_RegisterUrl;
private String m_WebHost;
private Integer m_WebPort;
private String m_YagHost;
private Integer m_YagPort;
private ArrayList<String> m_SkinDomains;
private String m_RsaPublicKeyFileName;
private String m_RsaPrivateKeyFileName;
private String m_SignaturePublicKey;
private String m_SignaturePrivateKey;
private Boolean m_debug;
private String m_EmailHost;
private String m_EmailPort;
private String m_EmailUsername;
private String m_EmailPassword;
private String m_EmailFrom;
IdentityVerification plugin;
FileConfiguration config;
}

View File

@ -0,0 +1,31 @@
package site.deercloud.identityverification.Controller;
import site.deercloud.identityverification.HttpServer.model.EmailCode;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public class EmailCodeCache {
public static Map<String, EmailCode> EMAIL_CODE_CACHE = new ConcurrentHashMap<>();
public static void addEmailCode(String email, String code) {
EMAIL_CODE_CACHE.put(email, new EmailCode(email, code));
}
public static Optional<EmailCode> getEmailCode(String email) {
return Optional.ofNullable(EMAIL_CODE_CACHE.get(email));
}
public static void removeEmailCode(String email) {
EMAIL_CODE_CACHE.remove(email);
}
public static boolean isEmailCodeExpired(String email) {
return getEmailCode(email).map(EmailCode::isExpired).orElse(true);
}
public static boolean isEmailCodeValid(String email, String code) {
return getEmailCode(email).map(emailCode -> emailCode.code.equals(code)).orElse(false);
}
}

View File

@ -0,0 +1,32 @@
package site.deercloud.identityverification.Controller;
import site.deercloud.identityverification.HttpServer.model.GameSession;
import site.deercloud.identityverification.IdentityVerification;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class GameSessionCache {
private static final Map<UUID, GameSession> ACTIVE_SESSIONS = new ConcurrentHashMap<>();
public static Optional<GameSession> getCacheSession(UUID playerUUID) {
return Optional.ofNullable(ACTIVE_SESSIONS.get(playerUUID));
}
public static void addSession(UUID playerUUID) {
ACTIVE_SESSIONS.put(playerUUID, new GameSession());
}
public static void removeSession(UUID playerUUID) {
// TODO: save to database
ACTIVE_SESSIONS.remove(playerUUID);
}
public GameSessionCache(IdentityVerification plugin) {
this.plugin = plugin;
}
private final IdentityVerification plugin;
}

View File

@ -0,0 +1,78 @@
package site.deercloud.identityverification.Controller;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import site.deercloud.identityverification.HttpServer.model.InviteCode;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.InviteCodeDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Utils.RandomCode;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Objects;
import java.util.Set;
public class InviteCodeManager {
// TODO: 邀请码管理器
// TODO: 1.生成邀请码
// TODO: 2.资格验证
// TODO: 3.把邀请码验证功能从数据移到这里
public static void CreateCode(CommandSender sender) {
String code = RandomCode.NewCodeWithAlphabet(8);
if (sender instanceof Player) {
if (sender.isOp()) {
Connection connection = SqlManager.getConnection();
try {
User console = UserDAO.selectByEmail(connection, "console@mc.com");
InviteCodeDAO.insert(connection, code, console.uuid, false, 0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
MyLogger.info(sender, "邀请码为:" + code);
} else {
MyLogger.warn(sender,"你没有的达到申请邀请码的要求!");
}
} else {
Connection connection = SqlManager.getConnection();
try {
User console = UserDAO.selectByEmail(connection, "console@mc.com");
InviteCodeDAO.insert(connection, code, console.uuid, false, 0);
} catch (SQLException e) {
throw new RuntimeException(e);
}
MyLogger.info("邀请码为:" + code);
}
}
public static void CreateCode(User user) {
}
public static void ListCodesOf(CommandSender sender) {
try {
String uuid;
Connection connection = SqlManager.getConnection();
if (sender instanceof Player) {
uuid = ((Player) sender).getUniqueId().toString();
} else {
uuid = Objects.requireNonNull(UserDAO.selectByEmail(connection, "console@mc.com")).uuid;
}
Set<InviteCode> codes = InviteCodeDAO.selectByInviter(connection, uuid);
sender.sendMessage("| 邀请码 | 创建时间 ");
for (InviteCode code : codes) {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(code.createTime);
sender.sendMessage("| " + code.code + " | " + date + " |");
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,50 @@
package site.deercloud.identityverification;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import site.deercloud.identityverification.Controller.AFKTracker;
import site.deercloud.identityverification.Controller.GameSessionCache;
import site.deercloud.identityverification.SQLite.BanListDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.WhiteListDAO;
import site.deercloud.identityverification.Controller.ConfigManager;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
public class Events implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) throws SQLException {
Player player = event.getPlayer();
String uuid = player.getUniqueId().toString();
Connection connection = SqlManager.getConnection();
if (!WhiteListDAO.isUuidInWhiteList(connection, uuid)) {
player.kickPlayer("你没有完成白名单实名认证,请前往 " + configManager.getHomePageUrl() + " 进行认证。");
}
Integer ban_record_id = BanListDAO.isBanned(connection, uuid);
if (ban_record_id > 0) {
String ban_reason = BanListDAO.getBanReasonById(connection, ban_record_id);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String ban_time = sdf.format(BanListDAO.getBanTimeById(connection, ban_record_id));
player.kickPlayer("你已被封禁,请联系管理员。原因:[ " + ban_reason + " ]" + " 至:" + ban_time);
}
GameSessionCache.addSession(event.getPlayer().getUniqueId());
}
@EventHandler
public void onPlayerQuit(PlayerJoinEvent event) throws SQLException {
GameSessionCache.removeSession(event.getPlayer().getUniqueId());
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) throws SQLException {
Player player = event.getPlayer();
IdentityVerification.getAfkTracker().performedAction(player.getUniqueId(), System.currentTimeMillis());
}
ConfigManager configManager = IdentityVerification.getInstance().getConfigManager();
}

View File

@ -0,0 +1,35 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.alibaba.fastjson.*;
import com.sun.net.httpserver.*;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.util.Map;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
import static site.deercloud.identityverification.Utils.Utils.*;
public class Ban implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
JSONObject jsonObject = JSONObject.parseObject(exchange.getRequestBody().toString());
String uuid = jsonObject.getString("uuid");
String reason = jsonObject.getString("reason");
Integer time = jsonObject.getInteger("time");
Boolean with_inviter = jsonObject.getBoolean("with_inviter");
} catch (IOException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,53 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.SQLite.InviteRelationDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
import static site.deercloud.identityverification.SQLite.SqlManager.getConnection;
import static site.deercloud.identityverification.Utils.Utils.*;
public class GetInviter implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")) {
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
String query = exchange.getRequestURI().getQuery();
Map<String, String> params = ParseQueryString(query);
Connection connection = getConnection();
String uuid = params.get("uuid");
String inviter_uuid = InviteRelationDAO.getInviterUUID(connection, uuid);
if (inviter_uuid == null) {
jsonResponse(exchange, 400, "找不到邀请者,这可能是个正版玩家。", null);
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("inviter_uuid", inviter_uuid);
jsonObject.put("inviter_name", UserDAO.selectByUuid(connection, inviter_uuid).uuid);
jsonResponse(exchange, 200, "OK", jsonObject);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,51 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.alibaba.fastjson.JSONArray;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
public class GetProfiles implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")) {
Response.err_method_not_allowed(exchange);
return;
}
JSONArray profiles_json = JSONArray.parseArray(exchange.getRequestBody().toString());
if (profiles_json.size() > 5) {
Response.success_no_content(exchange);
return;
}
Connection connection = SqlManager.getConnection();
ArrayList<Profile> profiles = new ArrayList<>();
for (int i = 0; i < profiles_json.size(); i++) {
Profile profile = ProfileDAO.selectByName(connection, profiles_json.getString(i));
if (profile != null) {
profiles.add(profile);
}
}
JSONArray response = new JSONArray();
for (Profile profile : profiles) {
response.add(profile.serialToJSONObject(false, false));
}
Response.success_200(exchange, response);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,55 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
public class Login implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")) {
Response.err_method_not_allowed(exchange);
return;
}
JSONObject request = JSONObject.parseObject(exchange.getRequestBody().toString());
String username = request.getString("username");
String password = request.getString("password");
if (username == null || password == null) {
jsonResponse(exchange, 500, "用户名或密码不能为空", null);
return;
}
Connection connection = SqlManager.getConnection();
User user = UserDAO.selectByEmail(connection, username);
if (user == null) {
jsonResponse(exchange, 500, "用户不存在", null);
return;
}
if (!user.password.equals(password)) {
jsonResponse(exchange, 500, "用户名或密码错误", null);
return;
}
// TODO: 2021/1/17 生成令牌
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,54 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.JsonArray;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
public class MetaData implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")){
exchange.sendResponseHeaders(405, 0);
exchange.close();
return;
}
String title = "A Minecraft Server";
String description = "Minecraft is a 3D sandbox game developed by Mojang Studios where players interact with a fully modifiable three-dimensional environment made of blocks and entities. ";
Boolean use_yag = true; // 是否使用外置登录
Boolean use_genuine = true; // 是否使用正版验证
Boolean need_invite = true; // 是否需要邀请码
Boolean use_whitelist = true; // 是否使用实名认证白名单
JSONArray links = new JSONArray();
JSONObject link = new JSONObject();
link.put("key", "author");
link.put("value", "https://blog.deercloud.site");
links.add(link);
link.put("key", "github");
link.put("value", "https://github.com/ColdeZhang");
links.add(link);
JSONObject jsonObject = new JSONObject();
jsonObject.put("title", title);
jsonObject.put("description", description);
jsonObject.put("links", links);
jsonObject.put("use_yag", use_yag);
jsonObject.put("use_genuine", use_genuine);
jsonObject.put("need_invite", need_invite);
jsonObject.put("use_whitelist", use_whitelist);
jsonResponse(exchange, 200, "OK", jsonObject);
} catch (IOException e) {
exchange.close();
}
}
}

View File

@ -0,0 +1,42 @@
package site.deercloud.identityverification.HttpServer.Api.Register;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.*;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.util.Map;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
import static site.deercloud.identityverification.Utils.Utils.*;
public class GetOnlineProfile implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")){
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
String query = exchange.getRequestURI().getQuery();
Map<String, String> params = ParseQueryString(query);
String name = params.get("name");
String UUID = getUUIDFromRemote(name);
if (UUID == null){
jsonResponse(exchange, 400, "ID不存在请检查输入。", null);
return;
}
JSONObject profile = getProfileFromRemote(UUID);
jsonResponse(exchange, 200, "获取正版信息成功!", profile);
} catch (IOException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,90 @@
package site.deercloud.identityverification.HttpServer.Api.Register;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.Controller.EmailCodeCache;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.HttpServer.model.Texture;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.InviteCodeDAO;
import site.deercloud.identityverification.SQLite.InviteRelationDAO;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.sql.Connection;
import java.util.UUID;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
import static site.deercloud.identityverification.SQLite.SqlManager.getConnection;
import static site.deercloud.identityverification.Utils.Utils.*;
public class Registration implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
// 设置响应头
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
JSONObject jsonObject = JSONObject.parseObject(exchange.getRequestBody().toString());
String email = jsonObject.getString("email");
String password = jsonObject.getString("password");
String active_code = jsonObject.getString("email_code"); // 邮箱验证码
String inviteCode = jsonObject.getString("invite_code");
String profile_name = jsonObject.getString("profile_name");
if (EmailCodeCache.isEmailCodeExpired(email)) {
jsonResponse(exchange, 500, "验证码无效,请重新获取。", null);
return;
}
if (!EmailCodeCache.isEmailCodeValid(email, active_code)) {
jsonResponse(exchange, 500, "验证码错误!", null);
return;
}
if (getUUIDFromRemote(profile_name) != null){
jsonResponse(exchange, 400, "此昵称已有正版玩家使用为避免ID碰撞请改名。", null);
return;
}
Connection connection = getConnection();
if (!InviteCodeDAO.isValid(connection, inviteCode)) {
jsonResponse(exchange, 400, "邀请码不存在或已被使用!", null);
return;
}
String new_uuid = UUID.randomUUID().toString();
String inviteCodeOwner = InviteCodeDAO.getInviterUUID(connection, inviteCode);
// 创建邀请关系
InviteRelationDAO.insert(connection, new_uuid, inviteCodeOwner, System.currentTimeMillis());
// 标记邀请码已使用
InviteCodeDAO.setUsed(connection, inviteCode, true, System.currentTimeMillis());
User user = new User();
user.uuid = new_uuid;
user.email = email;
user.password = password;
UserDAO.insert(connection, user);
Profile profile = new Profile();
profile.name = profile_name;
profile.uuid = UUID.randomUUID().toString();
profile.belongTo = user.uuid;
Texture texture = Texture.newDefault(profile.uuid);
profile.textures = texture.serialWithBase64();
profile.textures_signature = texture.sign();
ProfileDAO.insert(connection, profile);
jsonResponse(exchange, 200, "注册成功!", null);
} catch (Exception e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,42 @@
package site.deercloud.identityverification.HttpServer.Api.Register;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.Controller.EmailCodeCache;
import site.deercloud.identityverification.Utils.EmailSender;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Utils.RandomCode;
import java.io.IOException;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
public class SendEmailCode implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
JSONObject request = JSONObject.parseObject(exchange.getRequestBody().toString());
String email = request.getString("email");
if (!EmailCodeCache.isEmailCodeExpired(email)) {
jsonResponse(exchange, 500, "禁止频繁操作!", null);
return;
}
String code = RandomCode.NewCodeOnlyNumber(6);
EmailSender.sendCodeEmail(email, code);
EmailCodeCache.addEmailCode(email, code);
jsonResponse(exchange, 200, "发送成功,请在五分钟内完成注册。", null);
} catch (IOException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,50 @@
package site.deercloud.identityverification.HttpServer.Api.Register;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.SQLite.InviteCodeDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.UUID;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
import static site.deercloud.identityverification.Utils.Utils.ParseQueryString;
public class VerifyCode implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")){
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
String query = exchange.getRequestURI().getQuery();
Map<String, String> params = ParseQueryString(query);
String code = params.get("code");
Connection conn = SqlManager.getConnection();
if (!InviteCodeDAO.isValid(conn, code)){
jsonResponse(exchange, 400, "邀请码不可用!", null);
} else {
JSONObject jsonObject = new JSONObject();
String inviter_uuid = InviteCodeDAO.getInviterUUID(conn, code);
String inviter_name = UserDAO.selectByUuid(conn, inviter_uuid).email;
// 返回邀请者的名字
jsonObject.put("inviter_name", inviter_name);
jsonResponse(exchange, 200, "OK", jsonObject);
}
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,68 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.SQLite.WhiteListDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.util.Objects;
import static site.deercloud.identityverification.HttpServer.HttpServerManager.jsonResponse;
import static site.deercloud.identityverification.SQLite.SqlManager.getConnection;
import static site.deercloud.identityverification.Utils.Utils.*;
public class SignWhiteList implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
jsonResponse(exchange, 405, "Method Not Allowed", null);
return;
}
JSONObject jsonObject = JSONObject.parseObject(exchange.getRequestBody().toString());
Connection connection = getConnection();
String id = jsonObject.getString("id"); // 身份证实名认证用
String name = jsonObject.getString("name"); // 真实姓名实名认证用
String profile_name = jsonObject.getString("username"); // 昵称
Boolean is_genuine = jsonObject.getBoolean("is_genuine"); // 是否正版
String uuid;
if (is_genuine) {
// 正版玩家 - 从Mojiang服务器获取uuid
uuid = getUUIDFromRemote(profile_name);
} else {
// 离线玩家 - 从本地数据库获取uuid
Profile profile = ProfileDAO.selectByName(connection, profile_name);
if (profile == null) {
jsonResponse(exchange, 400, "玩家不存在", null);
return;
}
uuid = UserDAO.selectByUuid(connection, profile.belongTo).uuid;
}
if (uuid == null) {
jsonResponse(exchange, 400, "UUID校验不通过请检查后输入。", null);
return;
}
if (WhiteListDAO.isIdInWhiteList(connection, id.hashCode())) {
jsonResponse(exchange, 400, "请勿重复添加白名单,本服一人一号。", null);
return;
}
// TODO: 调实名认证接口 不通过直接返回错误
//
//
WhiteListDAO.insert(connection, uuid, is_genuine, id.hashCode());
jsonResponse(exchange, 200, "添加白名单成功!", null);
} catch (Exception e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,27 @@
package site.deercloud.identityverification.HttpServer.Api;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
public class Test implements HttpHandler {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
/*
* PS: 必须按顺序设置响应: 添加响应头, 发送响应码和内容长度, 写出响应内容, 关闭处理器
*/
// 响应内容
byte[] respContents = "Hello World".getBytes("UTF-8");
// 设置响应头
httpExchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
// 设置响应code和内容长度
httpExchange.sendResponseHeaders(200, respContents.length);
// 设置响应内容
httpExchange.getResponseBody().write(respContents);
// 关闭处理器, 同时将关闭请求和响应的输入输出流如果还没关闭
httpExchange.close();
}
}

View File

@ -0,0 +1,121 @@
package site.deercloud.identityverification.HttpServer;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import site.deercloud.identityverification.HttpServer.Api.Register.GetOnlineProfile;
import site.deercloud.identityverification.HttpServer.Api.Register.Registration;
import site.deercloud.identityverification.HttpServer.Api.Register.VerifyCode;
import site.deercloud.identityverification.HttpServer.Web.*;
import site.deercloud.identityverification.HttpServer.Api.*;
import site.deercloud.identityverification.HttpServer.Yggdrasil.MetaData;
import site.deercloud.identityverification.HttpServer.Yggdrasil.authserver.*;
import site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver.GetProfile;
import site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver.HasJoined;
import site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver.Join;
import site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver.SessionTokenCache;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Controller.ConfigManager;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HttpServerManager {
public HttpServerManager(IdentityVerification plugin) {
configManager = plugin.getConfigManager();
// 注册路由
try {
String web_host = configManager.getWebHost();
int web_port = configManager.getWebPort();
String yag_host = configManager.getYagHost();
int yag_port = configManager.getYagPort();
webServer = HttpServer.create(new InetSocketAddress(web_host, web_port), 0);
yagServer = HttpServer.create(new InetSocketAddress(yag_host, yag_port), 0);
webServer.createContext("/", new WebServer());
webServer.createContext("/api/test", new Test());
// 获取正版用户信息
webServer.createContext("/api/getOnlineProfile", new GetOnlineProfile());
// 验证邀请码 返回邀请者信息用于确认
webServer.createContext("/api/verifyCode", new VerifyCode());
// 封禁玩家
webServer.createContext("/api/ban", new Ban());
// 离线用户注册
webServer.createContext("/api/registration", new Registration());
// 获取邀请者信息
webServer.createContext("/api/getInviter", new GetInviter());
// 登记白名单
webServer.createContext("/api/signWhiteList", new SignWhiteList());
// Yggdrasil API 元数据
yagServer.createContext("/", new MetaData());
// 登录 - 使用密码进行身份验证并分配一个新的令牌
yagServer.createContext("/authserver/authenticate", new Authenticate());
// 刷新 - 吊销原令牌并颁发一个新的令牌
yagServer.createContext("/authserver/refresh", new Refresh());
// 验证 - 验证令牌是否有效
yagServer.createContext("/authserver/validate", new Validate());
// 吊销 - 吊销令牌
yagServer.createContext("/authserver/invalidate", new Invalidate());
// 客户端进入服务器 - 客户端使用令牌加入游戏
yagServer.createContext("/sessionserver/session/minecraft/join", new Join());
// 服务端验证客户端 - 服务端使用令牌验证客户端
yagServer.createContext("/sessionserver/session/minecraft/hasJoined", new HasJoined());
// 查询角色属性 - 查询指定的角色属性
yagServer.createContext("/sessionserver/session/minecraft/profile/", new GetProfile());
// 按名称批量查询角色 - 不包含属性 最大5
yagServer.createContext("/api/profiles/minecraft", new GetProfiles());
} catch (IOException e) {
MyLogger.debug(e);
}
this.startHttpServer();
}
public void startHttpServer(){
// 设置线程池
Executor executor = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, new java.util.concurrent.ArrayBlockingQueue<>(10));
webServer.setExecutor(executor);
yagServer.setExecutor(executor);
webServer.start();
yagServer.start();
MyLogger.info("网页服务启动在: " + configManager.getWebHost() + ":" + configManager.getWebPort());
MyLogger.info("外置登录启动在: " + configManager.getYagHost() + ":" + configManager.getYagPort());
}
public void stopHttpServer(){
if (webServer != null){
webServer.stop(0);
}
if (yagServer != null){
yagServer.stop(0);
}
}
public static void jsonResponse(HttpExchange exchange, int code, String message, JSONObject data) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", code);
jsonObject.put("msg", message);
jsonObject.put("data", data);
exchange.sendResponseHeaders(200, jsonObject.toString().getBytes().length);
exchange.getResponseBody().write(jsonObject.toString().getBytes());
exchange.getResponseBody().close();
}
public static SessionTokenCache getSessionCache() {
return sessionTokenCache;
}
private HttpServer webServer;
private HttpServer yagServer;
private ConfigManager configManager;
private static SessionTokenCache sessionTokenCache = new SessionTokenCache();
}

View File

@ -0,0 +1,47 @@
package site.deercloud.identityverification.HttpServer.Web;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.io.InputStream;
public class WebServer implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
String uri = exchange.getRequestURI().toString();
if (uri.equals("/")){
uri = "/index.html";
}
String file_type = uri.substring(uri.lastIndexOf(".") + 1);
InputStream file = IdentityVerification.getInstance().getResource("web" + uri);
if (file == null){
MyLogger.debug("File not found: " + uri);
exchange.sendResponseHeaders(404, 0);
exchange.close();
return;
}
switch (file_type) {
case "js" -> exchange.getResponseHeaders().add("Content-Type", "application/javascript; charset=UTF-8");
case "css" -> exchange.getResponseHeaders().add("Content-Type", "text/css; charset=UTF-8");
case "html" -> exchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
case "png" -> exchange.getResponseHeaders().add("Content-Type", "image/png; charset=UTF-8");
case "jpg" -> exchange.getResponseHeaders().add("Content-Type", "image/jpeg; charset=UTF-8");
case "gif" -> exchange.getResponseHeaders().add("Content-Type", "image/gif; charset=UTF-8");
default -> exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
}
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write(file.readAllBytes());
exchange.close();
} catch (IOException e) {
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,33 @@
package site.deercloud.identityverification.HttpServer.Web;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.io.InputStream;
public class WebTest implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
String uri = exchange.getRequestURI().toString();
// 按照uri从resource中读取文件返回
InputStream file = IdentityVerification.getInstance().getResource("web" + uri);
if (file == null){
MyLogger.debug("File not found: " + uri);
exchange.sendResponseHeaders(404, 0);
exchange.close();
return;
}
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write(file.readAllBytes());
exchange.close();
} catch (IOException e) {
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,41 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.JsonArray;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Controller.ConfigManager;
import java.io.IOException;
public class MetaData implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")) {
Response.err_method_not_allowed(exchange);
return;
}
ConfigManager configManager = IdentityVerification.getInstance().getConfigManager();
JSONObject meta_json = new JSONObject();
meta_json.put("serverName", configManager.getServerName());
meta_json.put("implementationName", configManager.getImplementationName());
meta_json.put("implementationVersion", configManager.getImplementationVersion());
JSONObject links_json = new JSONObject();
links_json.put("homepage", configManager.getHomePageUrl());
links_json.put("register", configManager.getRegisterUrl());
meta_json.put("links", links_json);
JsonArray skinDomains_json = new JsonArray();
for (String domain : configManager.getSkinDomains()) {
skinDomains_json.add(domain);
}
JSONObject response_json = new JSONObject();
response_json.put("meta", meta_json);
response_json.put("skinDomains", skinDomains_json);
response_json.put("signaturePublickey", configManager.getSignaturePublicKey());
Response.success_200(exchange, response_json);
}
}

View File

@ -0,0 +1,97 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.authserver;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.Token;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.UUID;
import static site.deercloud.identityverification.SQLite.SqlManager.getConnection;
import static site.deercloud.identityverification.HttpServer.model.Response.*;
public class Authenticate implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
Response.err_method_not_allowed(exchange);
return;
}
JSONObject body = JSONObject.parseObject(exchange.getRequestBody().toString());
String username = body.getString("username"); // 邮箱或其他凭证
String password = body.getString("password"); // 密码
String clientToken = body.getString("clientToken"); // 由客户端指定的令牌的 clientToken可选
Boolean requestUser = body.getBoolean("requestUser"); // 是否在响应中包含用户信息默认 false
JSONObject agent = body.getJSONObject("agent");
String agent_name = agent.getString("name");
String agent_version = agent.getString("version");
Connection connection = getConnection();
if (!UserDAO.checkPassword(connection, username, password)) {
err_password_wrong(exchange, "密码错误,或短时间内多次登录失败而被暂时禁止登录");
}
User user = UserDAO.selectByEmail(connection, username);
if (clientToken == null) {
clientToken = UUID.randomUUID().toString();
}
String accessToken = UUID.randomUUID().toString();
// 颁发令牌
Token token = new Token();
token.accessToken = accessToken;
token.clientToken = clientToken;
token.userUUID = user.uuid;
token.expiresAt = System.currentTimeMillis() + 432000000;
Profile profile = null;
// 若用户没有任何角色则为空
// 若用户仅有一个角色那么通常绑定到该角色
// 若用户有多个角色通常为空以便客户端进行选择也就是说如果绑定的角色为空则需要客户端进行角色选择
ArrayList<Profile> profiles = ProfileDAO.selectAllByBelongTo(connection, user.uuid);
if (profiles.size() == 1) {
profile = profiles.get(0);
token.profileUUID = profile.uuid;
} else {
token.profileUUID = "";
}
TokenDAO.insert(connection, token);
JSONArray availableProfiles = new JSONArray();
for (Profile p : profiles) {
availableProfiles.add(p.serialToJSONObject(false, true));
}
JSONObject response = new JSONObject();
response.put("accessToken", accessToken);
response.put("clientToken", clientToken);
response.put("availableProfiles", availableProfiles);
if (profile != null) {
response.put("selectedProfile", profile.serialToJSONObject(false, true));
}
if (requestUser) {
response.put("user", user.serialToJSONObject());
}
Response.success_200(exchange, response);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,39 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.authserver;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
public class Invalidate implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")) {
Response.err_method_not_allowed(exchange);
return;
}
JSONObject request = JSONObject.parseObject(exchange.getRequestBody().toString());
String accessToken = request.getString("accessToken");
String clientToken = request.getString("clientToken");
Connection connection = SqlManager.getConnection();
TokenDAO.deleteByAccessToken(connection, accessToken);
Response.success_no_content(exchange);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,83 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.authserver;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.Token;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
import java.util.UUID;
public class Refresh implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
// 设置响应头
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")) {
Response.err_method_not_allowed(exchange);
return;
}
Connection connection = SqlManager.getConnection();
JSONObject body = JSONObject.parseObject(exchange.getRequestBody().toString());
String accessToken = body.getString("accessToken");
String clientToken = body.getString("clientToken");
Boolean requestUser = body.getBoolean("requestUser");
if (TokenDAO.isAccessTokenExpired(connection, accessToken)) {
Response.err_invalid_token(exchange, "无效的令牌", "无效的令牌");
return;
}
if (clientToken != null && TokenDAO.isClientTokenExpired(connection, clientToken)) {
Response.err_invalid_token(exchange, "无效的令牌", "无效的令牌");
return;
}
Token new_token = TokenDAO.selectByAccessToken(connection, accessToken);
if (new_token == null) {
Response.err_invalid_token(exchange, "无效的令牌", "无效的令牌");
return;
}
// 删除原先的令牌 并生成新的令牌
TokenDAO.deleteByAccessToken(connection, accessToken);
new_token.accessToken = UUID.randomUUID().toString();
new_token.expiresAt = System.currentTimeMillis() + 432000000;
// 选择角色的操作 要求原令牌所绑定的角色为空
if (!body.getJSONObject("selectedProfile").isEmpty()) {
if (!Objects.equals(new_token.profileUUID, "")) {
Response.err_token_has_bind(exchange);
return;
}
// TODO: 校验角色是否存在
// 如果为空则绑定角色
new_token.profileUUID = body.getJSONObject("selectedProfile").getString("id");
}
// 新令牌存库
TokenDAO.insert(connection, new_token);
JSONObject response = new JSONObject();
response.put("accessToken", new_token.accessToken);
response.put("clientToken", new_token.clientToken);
if (requestUser) {
User user = UserDAO.selectByUuid(connection, new_token.userUUID);
response.put("user", user.serialToJSONObject());
}
if (!body.getJSONObject("selectedProfile").isEmpty()) {
response.put("selectedProfile", body.getJSONObject("selectedProfile"));
}
Response.success_200(exchange, response);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,50 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.authserver;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
public class SignOut implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")) {
Response.err_method_not_allowed(exchange);
return;
}
JSONObject request = JSONObject.parseObject(exchange.getRequestBody().toString());
String username = request.getString("username");
String password = request.getString("password");
Connection connection = SqlManager.getConnection();
if (!UserDAO.checkPassword(connection, username, password)) {
Response.err_password_wrong(exchange, null);
return;
}
User user = UserDAO.selectByEmail(connection, username);
// 吊销用户的所有令牌
ArrayList<Profile> profiles = ProfileDAO.selectAllByBelongTo(connection, user.uuid);
for (Profile profile : profiles) {
TokenDAO.deleteByProfileUUID(connection, profile.uuid);
}
Response.success_no_content(exchange);
} catch(SQLException | IOException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,53 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.authserver;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.Token;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
public class Validate implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
Response.err_method_not_allowed(exchange);
return;
}
Connection connection = SqlManager.getConnection();
JSONObject request = JSONObject.parseObject(exchange.getRequestBody().toString());
String accessToken = request.getString("accessToken");
String clientToken = request.getString("clientToken");
Token token = TokenDAO.selectByAccessToken(connection, accessToken);
// 检查令牌是否存在
if (token == null){
Response.err_invalid_token(exchange, "无效的令牌", "无效的令牌");
return;
}
// 检查 accessToken clientToken 是否匹配
if (clientToken != null && !Objects.equals(token.clientToken, clientToken)){
Response.err_invalid_token(exchange, "无效的令牌", "无效的令牌");
return;
}
// 检查令牌是否过期
if (TokenDAO.isAccessTokenExpired(connection, accessToken)) {
TokenDAO.deleteByAccessToken(connection, accessToken); // 如果过期则删除令牌
Response.err_invalid_token(exchange, "令牌过期", "令牌过期,请重新登录");
return;
}
Response.success_no_content(exchange);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,45 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import static site.deercloud.identityverification.Utils.Utils.ParseQueryString;
public class GetProfile implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")) {
Response.err_method_not_allowed(exchange);
return;
}
String uuid = exchange.getRequestURI().getPath().split("/")[4];
Map<String, String> request = ParseQueryString(exchange.getRequestURI().getQuery());
boolean unsigned = true;
if (request.containsKey("unsigned")) {
unsigned = Boolean.parseBoolean(request.get("unsigned"));
}
Connection connection = SqlManager.getConnection();
Profile profile = ProfileDAO.selectByUuid(connection, uuid);
if (profile == null) {
Response.success_no_content(exchange);
return;
}
Response.success_200(exchange, profile.serialToJSONObject(true, unsigned));
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,67 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.HttpServerManager;
import site.deercloud.identityverification.HttpServer.model.Profile;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.Token;
import site.deercloud.identityverification.SQLite.ProfileDAO;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.sql.Connection;
import java.util.Map;
import java.util.Objects;
import static site.deercloud.identityverification.Utils.Utils.ParseQueryString;
public class HasJoined implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("GET")) {
Response.err_method_not_allowed(exchange);
return;
}
Map<String, String> query = ParseQueryString(exchange.getRequestURI().getQuery());
String username = query.get("username");
String serverId = query.get("serverId");
String ip = query.get("ip");
Connection connection = SqlManager.getConnection();
// 验证缓存令牌有效性
if (HttpServerManager.getSessionCache().verifyIsExpire(serverId, ip)) {
Response.success_no_content(exchange);
return;
}
// 获取缓存令牌对应令牌
String accessToken = HttpServerManager.getSessionCache().getToken(serverId);
Token token = TokenDAO.selectByAccessToken(connection, accessToken);
if (token == null || token.profileUUID == null || !token.profileUUID.equals(username)) {
Response.success_no_content(exchange);
return;
}
// 获取令牌对应的角色
Profile profile = ProfileDAO.selectByUuid(connection, token.profileUUID);
if (profile == null) {
Response.success_no_content(exchange);
return;
}
// 检查角色名是否一致
if (!Objects.equals(username, profile.name)) {
Response.success_no_content(exchange);
return;
}
Response.success_200(exchange, profile.serialToJSONObject(true, true));
} catch (Exception e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,46 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import site.deercloud.identityverification.HttpServer.HttpServerManager;
import site.deercloud.identityverification.HttpServer.model.Response;
import site.deercloud.identityverification.HttpServer.model.Token;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.TokenDAO;
import site.deercloud.identityverification.Utils.MyLogger;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
public class Join implements HttpHandler {
@Override
public void handle(HttpExchange exchange){
try {
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
if (!exchange.getRequestMethod().equals("POST")){
Response.err_method_not_allowed(exchange);
return;
}
JSONObject request = JSONObject.parseObject(exchange.getRequestBody().toString());
String accessToken = request.getString("accessToken");
String selectedProfile = request.getString("selectedProfile");
String serverId = request.getString("serverId");
Connection connection = SqlManager.getConnection();
Token token = TokenDAO.selectByAccessToken(connection, accessToken);
if (token == null || token.profileUUID == null || !token.profileUUID.equals(selectedProfile)){
Response.err_invalid_token(exchange, "无效的令牌", "无效的令牌");
return;
}
// 缓存到内存中
HttpServerManager.getSessionCache().add(serverId, token.accessToken, exchange.getRemoteAddress().getHostString());
Response.success_no_content(exchange);
} catch (IOException | SQLException e) {
exchange.close();
MyLogger.debug(e);
}
}
}

View File

@ -0,0 +1,33 @@
package site.deercloud.identityverification.HttpServer.Yggdrasil.sessionserver;
import java.util.Map;
import java.util.Objects;
public class SessionTokenCache {
private Map<String, String> serverID_token;
private Map<String, String> serverID_ip;
private Map<String, Long> serverID_time;
public void add(String serverID, String token, String ip){
serverID_token.put(serverID, token);
serverID_ip.put(serverID, ip);
serverID_time.put(serverID, System.currentTimeMillis() + 30000);
}
public boolean verifyIsExpire(String serverID, String ip){
if (!serverID_token.containsKey(serverID)){
return true;
}
if (serverID_time.get(serverID) < System.currentTimeMillis()){
serverID_token.remove(serverID);
serverID_ip.remove(serverID);
serverID_time.remove(serverID);
return true;
}
return false;
}
public String getToken(String serverID){
return serverID_token.get(serverID);
}
}

View File

@ -0,0 +1,17 @@
package site.deercloud.identityverification.HttpServer.model;
public class EmailCode {
public String email;
public String code;
public Long expireTime;
public EmailCode(String email, String code) {
this.email = email;
this.code = code;
this.expireTime = System.currentTimeMillis() + 1000 * 60 * 5;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}

View File

@ -0,0 +1,24 @@
package site.deercloud.identityverification.HttpServer.model;
public class GameSession {
public GameSession() {
joinTime = System.currentTimeMillis();
lastActionTime = joinTime;
afkTime = 0L;
}
public void addAfkTime(long time) {
afkTime += time;
}
public void setLastMovement(long time) {
lastActionTime = time;
}
public long getLastMovement() {
return lastActionTime;
}
public Long joinTime;
public Long lastActionTime;
public Long afkTime;
}

View File

@ -0,0 +1,9 @@
package site.deercloud.identityverification.HttpServer.model;
public class InviteCode {
public String code;
public String inviter;
public Long createTime;
public Boolean isUsed;
public Long usedTime;
}

View File

@ -0,0 +1,66 @@
package site.deercloud.identityverification.HttpServer.model;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
public class Profile {
public String uuid;
public String belongTo;
public String name;
public String textures;
public String textures_signature;
public String uploadableTextures;
public String uploadableTextures_signature;
public JSONObject serialToJSONObject(Boolean with_properties,Boolean unsigned) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", uuid);
jsonObject.put("name", name);
if (!with_properties) {
return jsonObject;
}
JSONArray properties = new JSONArray();
JSONObject property = new JSONObject();
if (textures != null) {
property.put("name", "textures");
property.put("value", textures);
if (!unsigned) {
property.put("signature", textures_signature);
}
properties.add(property);
}
if (uploadableTextures != null) {
property.put("name", "uploadableTextures");
property.put("value", uploadableTextures);
if (!unsigned) {
property.put("signature", uploadableTextures_signature);
}
properties.add(property);
}
jsonObject.put("properties", properties);
return jsonObject;
}
public Profile(JSONObject jsonObject) {
uuid = jsonObject.getString("id");
name = jsonObject.getString("name");
JSONArray properties = jsonObject.getJSONArray("properties");
for (int i = 0; i < properties.size(); i++) {
JSONObject property = properties.getJSONObject(i);
if (property.containsKey("textures")) {
textures = property.getString("textures");
textures_signature = property.getString("textures_signature");
}
if (property.containsKey("uploadableTextures")) {
uploadableTextures = property.getString("uploadableTextures");
uploadableTextures_signature = property.getString("uploadableTextures_signature");
}
}
}
public Profile() {
}
}

View File

@ -0,0 +1,75 @@
package site.deercloud.identityverification.HttpServer.model;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
public class Response {
public static void err_invalid_token(HttpExchange exchange, String msg, String cause) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("error", "ForbiddenOperationException");
jsonObject.put("errorMessage", "Invalid token. " + msg);
jsonObject.put("cause", cause);
exchange.sendResponseHeaders(403, jsonObject.toString().getBytes().length);
exchange.getResponseBody().write(jsonObject.toString().getBytes());
exchange.getResponseBody().close();
}
public static void err_password_wrong(HttpExchange exchange, String cause) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("error", "ForbiddenOperationException");
jsonObject.put("errorMessage", "密码错误,或短时间内多次登录失败而被暂时禁止登录");
jsonObject.put("cause", cause);
exchange.sendResponseHeaders(403, jsonObject.toString().getBytes().length);
exchange.getResponseBody().write(jsonObject.toString().getBytes());
exchange.getResponseBody().close();
}
public static void success_200(HttpExchange exchange, JSONObject data) throws IOException {
exchange.sendResponseHeaders(200, data.toString().getBytes().length);
exchange.getResponseBody().write(data.toString().getBytes());
exchange.getResponseBody().close();
}
public static void success_200(HttpExchange exchange, JSONArray data) throws IOException {
exchange.sendResponseHeaders(200, data.toString().getBytes().length);
exchange.getResponseBody().write(data.toString().getBytes());
exchange.getResponseBody().close();
}
public static void err_method_not_allowed(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(405, "Method Not Allowed".getBytes().length);
exchange.getResponseBody().write("Method Not Allowed".getBytes());
exchange.getResponseBody().close();
}
public static void err_token_has_bind(HttpExchange exchange) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("error", "IllegalArgumentException");
jsonObject.put("errorMessage", "IAccess token already has a profile assigned.");
jsonObject.put("cause", "试图向一个已经绑定了角色的令牌指定其要绑定的角色");
exchange.sendResponseHeaders(400, jsonObject.toString().getBytes().length);
exchange.getResponseBody().write(jsonObject.toString().getBytes());
exchange.getResponseBody().close();
}
public static void success_no_content(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(204, "No Content".getBytes().length);
exchange.getResponseBody().write("No Content".getBytes());
exchange.getResponseBody().close();
}
public static void err_invalid_profile(HttpExchange exchange) throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("error", "ForbiddenOperationException");
jsonObject.put("errorMessage", "Invalid token.");
jsonObject.put("cause", "试图使用一个不存在的角色UUID来登录");
exchange.sendResponseHeaders(403, jsonObject.toString().getBytes().length);
exchange.getResponseBody().write(jsonObject.toString().getBytes());
exchange.getResponseBody().close();
}
}

View File

@ -0,0 +1,50 @@
package site.deercloud.identityverification.HttpServer.model;
import com.alibaba.fastjson.JSONObject;
import site.deercloud.identityverification.IdentityVerification;
import site.deercloud.identityverification.Utils.SignatureUtil;
import java.util.Base64;
import java.util.UUID;
public class Texture {
public String profileId;
public String profileName;
public Long timestamp;
public String skinUrl;
public String capeUrl;
public static Texture newDefault(String Name) {
Texture texture = new Texture();
texture.profileName = Name;
texture.profileId = UUID.randomUUID().toString();
texture.timestamp = System.currentTimeMillis();
texture.skinUrl = "https://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1";
texture.capeUrl = null;
return texture;
}
public String serialWithBase64() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("timestamp", timestamp);
jsonObject.put("profileId", profileId);
jsonObject.put("profileName", profileName);
JSONObject textures = new JSONObject();
JSONObject skin = new JSONObject();
skin.put("url", skinUrl);
textures.put("SKIN", skin);
if (capeUrl != null) {
JSONObject cape = new JSONObject();
cape.put("url", capeUrl);
textures.put("CAPE", cape);
}
jsonObject.put("textures", textures);
return Base64.getEncoder().encodeToString(jsonObject.toJSONString().getBytes());
}
public String sign() throws Exception {
// 使用SHA1withRSA算法签名
String private_key_str = IdentityVerification.getInstance().getConfigManager().getSignaturePrivateKey();
return SignatureUtil.sign(serialWithBase64(), private_key_str);
}
}

View File

@ -0,0 +1,10 @@
package site.deercloud.identityverification.HttpServer.model;
public class Token {
public String accessToken;
public String clientToken;
public String profileUUID;
public String userUUID;
public Long expiresAt;
}

View File

@ -0,0 +1,24 @@
package site.deercloud.identityverification.HttpServer.model;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
public class User {
public String uuid;
public String password;
public String email;
public Long createTime;
public Long updateTime;
public JSONObject serialToJSONObject() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", uuid);
JSONArray properties = new JSONArray();
JSONObject property = new JSONObject();
property.put("name", "preferredLanguage");
property.put("value", "zh_CN");
properties.add(property);
jsonObject.put("properties", properties);
return jsonObject;
}
}

View File

@ -0,0 +1,87 @@
package site.deercloud.identityverification;
import java.io.File;
import org.bukkit.plugin.java.JavaPlugin;
import site.deercloud.identityverification.HttpServer.HttpServerManager;
import site.deercloud.identityverification.HttpServer.model.User;
import site.deercloud.identityverification.SQLite.SqlManager;
import site.deercloud.identityverification.SQLite.UserDAO;
import site.deercloud.identityverification.Controller.AFKTracker;
import site.deercloud.identityverification.Controller.ConfigManager;
import site.deercloud.identityverification.Utils.MyLogger;
import site.deercloud.identityverification.Controller.GameSessionCache;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.UUID;
public final class IdentityVerification extends JavaPlugin {
@Override
public void onEnable() {
// Plugin startup logic
instance = this;
configManager = new ConfigManager(this);
httpServerManager = new HttpServerManager(this);
gameSessionCache = new GameSessionCache(this);
afkTracker = new AFKTracker(this);
// 初始化数据表
try {
Connection connection = SqlManager.getConnection();
SqlManager.createTables(connection);
if (!UserDAO.isEmailExist(connection, "console@mc.com")) {
// 生成控制台用户
User consoleUser = new User();
consoleUser.email = "console@mc.com";
consoleUser.uuid = UUID.randomUUID().toString();
consoleUser.password = String.valueOf("123456".hashCode());
UserDAO.insert(connection, consoleUser);
}
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
// 初始化RSA
String publicKey = new File(this.getDataFolder(), configManager.getPublicKeyFileName()).toString();
String privateKey = new File(this.getDataFolder(), configManager.getPrivateKeyFileName()).toString();
if (publicKey == null || privateKey == null) {
MyLogger.error("RSA文件不存在, 插件退出。");
this.getServer().getPluginManager().disablePlugin(this);
}else {
privateKey = privateKey.replace("-----BEGIN OPENSSH PRIVATE KEY-----", "");
privateKey = privateKey.replace("-----END OPENSSH PRIVATE KEY-----", "");
configManager.setSignaturePublicKey(publicKey);
configManager.setSignaturePrivateKey(privateKey);
}
}
@Override
public void onDisable() {
// Plugin shutdown logic
if (httpServerManager != null) {
httpServerManager.stopHttpServer();
}
}
public ConfigManager getConfigManager() {
return configManager;
}
public static IdentityVerification getInstance() {
return instance;
}
public static GameSessionCache getSessionCache() {
return gameSessionCache;
}
public static AFKTracker getAfkTracker() {
return afkTracker;
}
private HttpServerManager httpServerManager;
private ConfigManager configManager;
private static GameSessionCache gameSessionCache;
private static AFKTracker afkTracker;
private static IdentityVerification instance;
}

View File

@ -0,0 +1,76 @@
package site.deercloud.identityverification.SQLite;
import java.sql.*;
public class BanListDAO {
public static void createTable(Connection con) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS ban_list (\n"
+ " id integer PRIMARY KEY AUTOINCREMENT ,\n" // ID
+ " uuid text NOT NULL ,\n" // UUID
+ " name text NOT NULL,\n" // 玩家名字
+ " ban_time integer NOT NULL,\n" // 封禁时间
+ " ban_reason text NOT NULL,\n" // 封禁原因
+ " create_time integer NOT NULL\n" // 数据创建时间
+ ");";
Statement stat = null;
stat = con.createStatement();
stat.executeUpdate(sql);
}
public static void insert(Connection con, String uuid, String name, long banTime, String banReason, long createTime) throws SQLException {
String sql = "INSERT INTO ban_list(uuid,name,ban_time,ban_reason,create_time) VALUES(?,?,?,?,?)";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, uuid);
prep.setString(2, name);
prep.setLong(3, banTime);
prep.setString(4, banReason);
prep.setLong(5, createTime);
prep.executeUpdate();
}
public static Integer isBanned(Connection con, String uuid) throws SQLException {
String sql = "SELECT ban_time FROM ban_list WHERE uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, uuid);
ResultSet rs = prep.executeQuery();
while (rs.next()) {
if(rs.getLong("ban_time") > System.currentTimeMillis()) {
return rs.getInt("id");
}
}
return -1;
}
public static String getBanReasonById(Connection con, Integer id) throws SQLException {
String sql = "SELECT ban_reason FROM ban_list WHERE id = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setInt(1, id);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return rs.getString("ban_reason");
}
return null;
}
public static long getBanTimeById(Connection con, Integer id) throws SQLException {
String sql = "SELECT ban_time FROM ban_list WHERE id = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setInt(1, id);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return rs.getLong("ban_time");
}
return -1;
}
public static String getNameById(Connection con, Integer id) throws SQLException {
String sql = "SELECT name FROM ban_list WHERE id = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setInt(1, id);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return rs.getString("name");
}
return null;
}
}

View File

@ -0,0 +1,92 @@
package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.InviteCode;
import java.sql.*;
import java.util.Set;
public class InviteCodeDAO {
public static void createTable(Connection con) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS invite_code (\n"
+ " code text PRIMARY KEY,\n" // 邀请码
+ " inviter_uuid text NOT NULL,\n" // 所有者
+ " create_time integer NOT NULL,\n" // 创建时间
+ " is_used integer NOT NULL,\n" // 是否被使用
+ " use_time integer NOT NULL\n" // 使用时间
+ ");";
Statement stat = null;
stat = con.createStatement();
stat.executeUpdate(sql);
}
public static void insert(Connection con, String code, String inviterUUID, boolean isUsed, long useTime) throws SQLException {
String sql = "INSERT INTO invite_code(code,inviter_uuid,create_time,is_used,use_time) VALUES(?,?,?,?,?)";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, code);
prep.setString(2, inviterUUID);
prep.setLong(3, System.currentTimeMillis());
prep.setBoolean(4, isUsed);
prep.setLong(5, useTime);
prep.executeUpdate();
}
public static String getInviterUUID(Connection con, String code) throws SQLException {
String sql = "SELECT inviter_uuid FROM invite_code WHERE code = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, code);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return rs.getString("inviter_uuid");
}
return null;
}
public static boolean isValid(Connection con, String code) throws SQLException {
String sql = "SELECT is_used FROM invite_code WHERE code = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, code);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return !rs.getBoolean("is_used");
}
return false;
}
public static void setUsed(Connection con, String code, boolean isUsed, long useTime) throws SQLException {
String sql = "UPDATE invite_code SET is_used = ?,use_time = ? WHERE code = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setBoolean(1, isUsed);
prep.setLong(2, useTime);
prep.setString(3, code);
prep.executeUpdate();
}
public static long getUseTime(Connection con, String code) throws SQLException {
String sql = "SELECT use_time FROM invite_code WHERE code = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, code);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return rs.getLong("use_time");
}
return 0;
}
public static Set<InviteCode> selectByInviter(Connection con, String inviterUUID) throws SQLException {
String sql = "SELECT * FROM invite_code WHERE inviter_uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, inviterUUID);
ResultSet rs = prep.executeQuery();
Set<InviteCode> inviteCodes = null;
while (rs.next()) {
InviteCode inviteCode = new InviteCode();
inviteCode.code = rs.getString("code");
inviteCode.inviter = rs.getString("inviter_uuid");
inviteCode.createTime = rs.getLong("create_time");
inviteCode.isUsed = rs.getBoolean("is_used");
inviteCode.usedTime = rs.getLong("use_time");
inviteCodes.add(inviteCode);
}
return inviteCodes;
}
}

View File

@ -0,0 +1,38 @@
package site.deercloud.identityverification.SQLite;
import java.sql.*;
public class InviteRelationDAO {
public static void createTable(Connection con) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS invite_relation (\n"
+ " invitee_uuid text PRIMARY KEY,\n" // 被邀请人UUID
+ " inviter_uuid text NOT NULL,\n" // 邀请人UUID
+ " invite_time integer NOT NULL\n" // 邀请时间
+ ");";
Statement stat = null;
stat = con.createStatement();
stat.executeUpdate(sql);
}
public static void insert(Connection con, String inviteeUUID, String inviterUUID, long inviteTime) throws SQLException {
String sql = "INSERT INTO invite_relation(invitee_uuid,inviter_uuid,invite_time) VALUES(?,?,?)";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, inviteeUUID);
prep.setString(2, inviterUUID);
prep.setLong(3, inviteTime);
prep.executeUpdate();
}
public static String getInviterUUID(Connection con, String inviteeUUID) throws SQLException {
String sql = "SELECT inviter_uuid FROM invite_relation WHERE invitee_uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, inviteeUUID);
ResultSet rs = prep.executeQuery();
if (rs.next()) {
return rs.getString("inviter_uuid");
}
return null;
}
}

View File

@ -0,0 +1,107 @@
package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.Profile;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
public class ProfileDAO {
public static void createTable(Connection connection) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS profile (\n"
+ " uuid text PRIMARY KEY,\n" // UUID
+ " belong_to text NOT NULL,\n" // 所属User
+ " name text NOT NULL,\n" // 玩家名字
+ " textures text NOT NULL,\n" // 材质
+ " textures_signature text NOT NULL,\n" // 材质签名
+ " uploadableTextures text NOT NULL,\n" // 可上传的材质
+ " uploadableTextures_signature text NOT NULL,\n" // 可上传的材质签名
+ " update_time integer NOT NULL\n" // 数据更新时间
+ ");";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.execute();
}
public static void insert(Connection connection, Profile profile) throws SQLException {
String sql = "INSERT INTO profile(uuid,belong_to,name,textures,textures_signature,uploadableTextures,uploadableTextures_signature,update_time) VALUES(?,?,?,?,?,?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, profile.uuid);
preparedStatement.setString(2, profile.belongTo);
preparedStatement.setString(3, profile.name);
preparedStatement.setString(4, profile.textures);
preparedStatement.setString(5, profile.textures_signature);
preparedStatement.setString(6, profile.uploadableTextures);
preparedStatement.setString(7, profile.uploadableTextures_signature);
preparedStatement.setLong(8, System.currentTimeMillis());
preparedStatement.executeUpdate();
}
public static void update(Connection connection, Profile profile) throws SQLException {
String sql = "UPDATE profile SET name = ?, textures = ?, textures_signature = ?, uploadableTextures = ?, uploadableTextures_signature = ?, update_time = ? WHERE uuid = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, profile.name);
preparedStatement.setString(2, profile.textures);
preparedStatement.setString(3, profile.textures_signature);
preparedStatement.setString(4, profile.uploadableTextures);
preparedStatement.setString(5, profile.uploadableTextures_signature);
preparedStatement.setLong(6, System.currentTimeMillis());
preparedStatement.setString(7, profile.uuid);
preparedStatement.executeUpdate();
}
public static Profile selectByUuid(Connection connection, String uuid) throws SQLException {
String sql = "SELECT * FROM profile WHERE uuid = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, uuid);
Profile profile = new Profile();
if (preparedStatement.executeQuery().next()) {
profile.uuid = preparedStatement.getResultSet().getString("uuid");
profile.belongTo = preparedStatement.getResultSet().getString("belong_to");
profile.name = preparedStatement.getResultSet().getString("name");
profile.textures = preparedStatement.getResultSet().getString("textures");
profile.textures_signature = preparedStatement.getResultSet().getString("textures_signature");
profile.uploadableTextures = preparedStatement.getResultSet().getString("uploadableTextures");
profile.uploadableTextures_signature = preparedStatement.getResultSet().getString("uploadableTextures_signature");
return profile;
}
return null;
}
public static ArrayList<Profile> selectAllByBelongTo(Connection connection, String belongTo) throws SQLException {
String sql = "SELECT * FROM profile WHERE belong_to = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, belongTo);
ArrayList<Profile> profiles = new ArrayList<>();
while (preparedStatement.executeQuery().next()) {
Profile profile = new Profile();
profile.uuid = preparedStatement.executeQuery().getString("uuid");
profile.belongTo = belongTo;
profile.name = preparedStatement.executeQuery().getString("name");
profile.textures = preparedStatement.executeQuery().getString("textures");
profile.textures_signature = preparedStatement.executeQuery().getString("textures_signature");
profile.uploadableTextures = preparedStatement.executeQuery().getString("uploadableTextures");
profile.uploadableTextures_signature = preparedStatement.executeQuery().getString("uploadableTextures_signature");
profiles.add(profile);
}
return profiles;
}
public static Profile selectByName(Connection connection, String name) throws SQLException {
String sql = "SELECT * FROM profile WHERE name = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, name);
Profile profile = new Profile();
if (preparedStatement.executeQuery().next()) {
profile.uuid = preparedStatement.getResultSet().getString("uuid");
profile.belongTo = preparedStatement.getResultSet().getString("belong_to");
profile.name = name;
profile.textures = preparedStatement.getResultSet().getString("textures");
profile.textures_signature = preparedStatement.getResultSet().getString("textures_signature");
profile.uploadableTextures = preparedStatement.getResultSet().getString("uploadableTextures");
profile.uploadableTextures_signature = preparedStatement.getResultSet().getString("uploadableTextures_signature");
return profile;
}
return null;
}
}

View File

@ -0,0 +1,40 @@
package site.deercloud.identityverification.SQLite;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;
import java.sql.*;
public class SqlManager {
public static Connection getConnection(){
try {
SQLiteConfig config = new SQLiteConfig();
config.setSharedCache(true);
config.enableRecursiveTriggers(true);
SQLiteDataSource ds = new SQLiteDataSource(config);
String url = System.getProperty("user.dir"); // 获取工作目录
ds.setUrl("jdbc:sqlite:"+url+"/plugins/IdentityVerification/"+"IV-Database.db");
return ds.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static void createTables(Connection connection) {
try {
BanListDAO.createTable(connection);
InviteCodeDAO.createTable(connection);
UserDAO.createTable(connection);
InviteRelationDAO.createTable(connection);
ProfileDAO.createTable(connection);
WhiteListDAO.createTable(connection);
TokenDAO.createTable(connection);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,98 @@
package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.Token;
import java.sql.*;
public class TokenDAO {
public static void createTable(Connection connection) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS token (\n"
+ " access_token text PRIMARY KEY,\n"
+ " client_token text NOT NULL,\n"
+ " user_uuid text NOT NULL,\n"
+ " profile_uuid text NOT NULL,\n"
+ " expires_at integer NOT NULL\n"
+ ");";
Statement statement = connection.createStatement();
statement.execute(sql);
}
public static void insert(Connection connection, Token token) throws SQLException {
String sql = "INSERT INTO token(access_token,client_token,user_uuid,profile_uuid,expires_at) VALUES(?,?,?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, token.accessToken);
preparedStatement.setString(2, token.clientToken);
preparedStatement.setString(3, token.userUUID);
preparedStatement.setString(4, token.profileUUID);
preparedStatement.setLong(5, token.expiresAt);
preparedStatement.executeUpdate();
}
public static Boolean isAccessTokenExpired(Connection connection, String accessToken) throws SQLException {
String sql = "SELECT * FROM token WHERE access_token = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, accessToken);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
return resultSet.getLong("expires_at") < System.currentTimeMillis();
} else {
return true;
}
}
public static Boolean isClientTokenExpired(Connection connection, String clientToken) throws SQLException {
String sql = "SELECT * FROM token WHERE client_token = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, clientToken);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
return resultSet.getLong("expires_at") < System.currentTimeMillis();
} else {
return true;
}
}
public static Boolean isTokenBindProfile(Connection connection, String accessToken) throws SQLException {
String sql = "SELECT * FROM token WHERE access_token = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, accessToken);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
return resultSet.getString("profile_uuid") != null;
} else {
return true;
}
}
public static Token selectByAccessToken(Connection connection, String accessToken) throws SQLException {
String sql = "SELECT * FROM token WHERE access_token = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, accessToken);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
Token token = new Token();
token.accessToken = resultSet.getString("access_token");
token.clientToken = resultSet.getString("client_token");
token.userUUID = resultSet.getString("user_uuid");
token.profileUUID = resultSet.getString("profile_uuid");
token.expiresAt = resultSet.getLong("expires_at");
return token;
} else {
return null;
}
}
public static void deleteByAccessToken(Connection connection, String accessToken) throws SQLException {
String sql = "DELETE FROM token WHERE access_token = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, accessToken);
preparedStatement.executeUpdate();
}
public static void deleteByProfileUUID(Connection connection, String profileUUID) throws SQLException {
String sql = "DELETE FROM token WHERE profile_uuid = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, profileUUID);
preparedStatement.executeUpdate();
}
}

View File

@ -0,0 +1,98 @@
package site.deercloud.identityverification.SQLite;
import site.deercloud.identityverification.HttpServer.model.User;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
public class UserDAO {
public static void createTable(Connection con) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS user (\n"
+ " uuid text PRIMARY KEY,\n" // UUID
+ " email text NOT NULL,\n" // 邮箱
+ " password text NOT NULL,\n" // 密码
+ " create_time integer NOT NULL,\n" // 数据创建时间
+ " update_time integer NOT NULL\n" // 数据更新时间
+ ");";
Statement stat;
stat = con.createStatement();
stat.executeUpdate(sql);
}
public static void insert(Connection con, User user) throws SQLException {
String sql = "INSERT INTO user(uuid, email, password, create_time, update_time) VALUES(?,?,?,?,?,?)";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, user.uuid);
prep.setString(2, user.email);
prep.setString(3, user.password);
prep.setLong(4, System.currentTimeMillis());
prep.setLong(5, System.currentTimeMillis());
prep.executeUpdate();
}
public static void update(Connection con, User user) throws SQLException {
String sql = "UPDATE user SET email = ?, password = ?, update_time = ? WHERE uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, user.email);
prep.setString(2, user.password);
prep.setLong(3, System.currentTimeMillis());
prep.setString(4, user.uuid);
prep.executeUpdate();
}
public static void delete(Connection con, String uuid) throws SQLException {
String sql = "DELETE FROM user WHERE uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, uuid);
prep.executeUpdate();
}
public static User selectByUuid(Connection con, String uuid) throws SQLException {
String sql = "SELECT * FROM user WHERE uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, uuid);
User user = new User();
user.uuid = uuid;
user.email = prep.executeQuery().getString("email");
user.password = prep.executeQuery().getString("password");
user.createTime = prep.executeQuery().getLong("create_time");
user.updateTime = prep.executeQuery().getLong("update_time");
return user;
}
public static User selectByEmail(Connection con, String email) throws SQLException {
String sql = "SELECT * FROM user WHERE email = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, email);
if (prep.executeQuery().next()) {
User user = new User();
user.uuid = prep.executeQuery().getString("uuid");
user.email = email;
user.password = prep.executeQuery().getString("password");
user.createTime = prep.executeQuery().getLong("create_time");
user.updateTime = prep.executeQuery().getLong("update_time");
return user;
} else {
return null;
}
}
public static Boolean checkPassword(Connection con, String email, String password) throws SQLException {
String sql = "SELECT * FROM user WHERE email = ?";
PreparedStatement prep = con.prepareStatement(sql);
if (prep.executeQuery().next()) {
return prep.executeQuery().getString("password").equals(password);
} else {
return false;
}
}
public static Boolean isEmailExist(Connection con, String email) throws SQLException {
String sql = "SELECT * FROM user WHERE email = ?";
PreparedStatement prep = con.prepareStatement(sql);
return prep.executeQuery().next();
}
}

View File

@ -0,0 +1,44 @@
package site.deercloud.identityverification.SQLite;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
public class WhiteListDAO {
public static void createTable(Connection con) throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS white_list (\n"
+ " uuid text PRIMARY KEY,\n" // UUID
+ " is_genuine integer NOT NULL,\n" // 是否为正版玩家
+ " id_hash integer NOT NULL,\n" // 身份证号哈希值
+ " create_time integer NOT NULL\n" // 数据创建时间
+ ");";
Statement stat;
stat = con.createStatement();
stat.executeUpdate(sql);
}
public static void insert(Connection con, String uuid, boolean isGenuine, Integer idHash) throws SQLException {
String sql = "INSERT INTO white_list(uuid,is_genuine,id_hash,create_time) VALUES(?,?,?,?)";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, uuid);
prep.setBoolean(2, isGenuine);
prep.setInt(3, idHash);
prep.setLong(4, System.currentTimeMillis());
prep.executeUpdate();
}
public static boolean isUuidInWhiteList(Connection con, String uuid) throws SQLException {
String sql = "SELECT * FROM white_list WHERE uuid = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setString(1, uuid);
return prep.executeQuery().next();
}
public static boolean isIdInWhiteList(Connection con, Integer idHash) throws SQLException {
String sql = "SELECT * FROM white_list WHERE id_hash = ?";
PreparedStatement prep = con.prepareStatement(sql);
prep.setInt(1, idHash);
return prep.executeQuery().next();
}
}

View File

@ -0,0 +1,81 @@
package site.deercloud.identityverification.Utils;
import site.deercloud.identityverification.IdentityVerification;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Date;
import java.util.Properties;
public class EmailSender {
static String host = IdentityVerification.getInstance().getConfigManager().getEmailHost(); // smtp服务器
static String port = IdentityVerification.getInstance().getConfigManager().getEmailPort(); // 端口
static String from = IdentityVerification.getInstance().getConfigManager().getEmailFrom(); // 发件人的email
static String account = IdentityVerification.getInstance().getConfigManager().getEmailUsername(); // 账号
static String password = IdentityVerification.getInstance().getConfigManager().getEmailPassword(); // 密码
public static boolean sendTestEmail(String to) {
String subject = "测试邮件";
String content = "这是一封测试邮件";
return sendEmail(to, subject, content);
}
public static boolean sendResetPasswordEmail(String to, String password) {
String subject = "[IV] 重置密码";
String content = "您的新密码为:" + password + ",请尽快登录并修改密码";
return sendEmail(to, subject, content);
}
public static boolean sendCodeEmail(String to, String code) {
String subject = "[IV] 验证码";
String content = "您的验证码为:" + code + "请在5分钟内输入";
return sendEmail(to, subject, content);
}
private static boolean sendEmail(String to, String subject, String content) {
try {
// 1. 创建参数配置, 用于连接邮件服务器的参数配置
Properties props = new Properties(); // 参数配置
props.setProperty("mail.transport.protocol", "smtp"); // 使用的协议JavaMail规范要求
props.setProperty("mail.smtp.host", host); // 发件人的邮箱的 SMTP 服务器地址
props.setProperty("mail.smtp.auth", "true"); // 需要请求认证
props.setProperty("mail.smtp.port", port);
props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.setProperty("mail.smtp.socketFactory.fallback", "false");
props.setProperty("mail.smtp.socketFactory.port", port);
// 2. 根据配置创建会话对象, 用于和邮件服务器交互
Session session = Session.getInstance(props);
session.setDebug(IdentityVerification.getInstance().getConfigManager().getDebug()); // 设置为debug模式, 可以查看详细的发送 log
// 3. 创建一封邮件
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(from, "IdentityVerification", "UTF-8")); // 发件人
message.setRecipient(Message.RecipientType.TO, new InternetAddress(to, "no-reply", "UTF-8")); // 收件人
message.setSubject(subject, "UTF-8"); // 邮件主题
message.setContent(content, "text/html;charset=UTF-8"); // 邮件正文可以使用html标签
message.setSentDate(new Date()); // 设置发件时间
message.saveChanges(); // 保存设置
// 4. 根据 Session 获取邮件传输对象
Transport transport = session.getTransport();
// 5. 使用 邮箱账号 密码 连接邮件服务器
// 这里认证的邮箱必须与 message 中的发件人邮箱一致否则报错
transport.connect(account, password);
// 6. 发送邮件, 发到所有的收件地址, message.getAllRecipients() 获取到的是在创建邮件对象时添加的所有收件人, 抄送人, 密送人
transport.sendMessage(message, message.getAllRecipients());
// 7. 关闭连接
transport.close();
return true;
} catch (Exception e) {
MyLogger.debug(e);
return false;
}
}
}

View File

@ -0,0 +1,56 @@
package site.deercloud.identityverification.Utils;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import site.deercloud.identityverification.IdentityVerification;
public class MyLogger {
private static IdentityVerification plugin = IdentityVerification.getInstance();
public static void info(String message) {
plugin.getLogger().info(ChatColor.GREEN + "| " +message);
}
public static void warn(String message) {
plugin.getLogger().warning(ChatColor.YELLOW + "| " +message);
}
public static void error(String message) {
plugin.getLogger().severe(ChatColor.RED + "| " +message);
}
public static void debug(String message) {
if (plugin.getConfigManager().getDebug()) {
plugin.getLogger().info(ChatColor.AQUA + "| " +message);
}
}
public static void debug(Exception e) {
if (plugin.getConfigManager().getDebug()) {
plugin.getLogger().info(ChatColor.AQUA + "| " + e.getMessage());
}
}
public static void info(CommandSender sender, String message) {
plugin.getLogger().info(ChatColor.GREEN + "[ 来自 " + sender.getName() + " 的消息 ] " + message);
sender.sendMessage(ChatColor.GREEN + "| " +message);
}
public static void warn(CommandSender sender, String message) {
plugin.getLogger().warning(ChatColor.YELLOW + "[ 来自 " + sender.getName() + " 的消息 ] " + message);
sender.sendMessage(ChatColor.YELLOW + "| " +message);
}
public static void error(CommandSender sender, String message) {
plugin.getLogger().severe(ChatColor.RED + "[ 来自 " + sender.getName() + " 的消息 ] " + message);
sender.sendMessage(ChatColor.RED + "| " +message);
}
public static void debug(CommandSender sender, String message) {
if (plugin.getConfigManager().getDebug()) {
plugin.getLogger().info(ChatColor.AQUA + "[ 来自 " + sender.getName() + " 的消息 ] " + message);
sender.sendMessage(ChatColor.AQUA + "| " +message);
}
}
}

View File

@ -0,0 +1,25 @@
package site.deercloud.identityverification.Utils;
public class RandomCode {
public static String NewCodeWithAlphabet(int length) {
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
int random = (int) (Math.random() * 36);
if (random < 10) {
code.append(random);
} else {
code.append((char) (random + 87));
}
}
return code.toString();
}
public static String NewCodeOnlyNumber(int length) {
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
int random = (int) (Math.random() * 10);
code.append(random);
}
return code.toString();
}
}

View File

@ -0,0 +1,96 @@
package site.deercloud.identityverification.Utils;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class SignatureUtil {
private final static String SIGN_TYPE_RSA = "RSA";
private final static String SIGN_ALGORITHMS = "SHA1WithRSA";
private final static String CHARSETTING = "UTF-8";
/**
* 获取私钥PKCS8格式需base64
* @param algorithm
* @param priKey
* @return PrivateKey
* @throws Exception
*/
public static PrivateKey getPrivateKeyFromPKCS8(String algorithm, String priKey) throws Exception {
if (algorithm == null || "".equals(algorithm) || priKey == null || "".equals(priKey))
return null;
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
byte[] encodedKey = StreamUtil.readText(new ByteArrayInputStream(priKey.getBytes())).getBytes();
encodedKey = Base64.getDecoder().decode(priKey.getBytes());
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
}
/**
* 通过证书获取公钥需BASE64X509为通用证书标准
* @param algorithm
* @param pubKey
* @return PublicKey
* @throws Exception
*/
public static PublicKey getPublicKeyFromX509(String algorithm, String pubKey) throws Exception {
if (algorithm == null || "".equals(algorithm) || pubKey == null || "".equals(pubKey))
return null;
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
StringWriter writer = new StringWriter();
StreamUtil.io(new InputStreamReader(new ByteArrayInputStream(pubKey.getBytes())), writer);
byte[] encodeByte = writer.toString().getBytes();
encodeByte = Base64.getDecoder().decode(pubKey.getBytes());
return keyFactory.generatePublic(new X509EncodedKeySpec(encodeByte));
}
/**
* 使用私钥对字符进行签名
* @param plain 内容体
* @param prikey 私钥
* @return String
* @throws Exception
*/
public static String sign(String plain, String prikey) throws Exception {
if (plain == null || "".equals(plain) || prikey == null || "".equals(prikey))
return null;
PrivateKey privatekey = getPrivateKeyFromPKCS8(SIGN_TYPE_RSA, prikey);
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initSign(privatekey);
signature.update(plain.getBytes(CHARSETTING));
byte[] signed = signature.sign();
return new String(Base64.getEncoder().encode(signed));
}
/**
* 将内容体签名信息及对方公钥进行验签
* @param plain 内容体
* @param sign 签名信息
* @param pubkey 对方公钥
* @return boolean
* @throws Exception
*/
public static boolean virefy(String plain, String sign, String pubkey) throws Exception {
if (plain == null || "".equals(plain) || sign == null || "".equals(sign) || pubkey == null || "".equals(pubkey))
return false;
PublicKey publicKey = getPublicKeyFromX509(SIGN_TYPE_RSA, pubkey);
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initVerify(publicKey);
signature.update(plain.getBytes(CHARSETTING));
return signature.verify(Base64.getDecoder().decode(sign.getBytes()));
}
}

View File

@ -0,0 +1,68 @@
package site.deercloud.identityverification.Utils;
import java.io.*;
public class StreamUtil {
private static final int DEFAULT_BUFFER_SIZE = 8192;
public static void io(InputStream in, OutputStream out) throws IOException {
io(in, out, -1);
}
public static void io(InputStream in, OutputStream out, int bufferSize) throws IOException {
if (bufferSize == -1) {
bufferSize = DEFAULT_BUFFER_SIZE;
}
byte[] buffer = new byte[bufferSize];
int amount;
while ((amount = in.read(buffer)) >= 0) {
out.write(buffer, 0, amount);
}
}
public static void io(Reader in, Writer out) throws IOException {
io(in, out, -1);
}
public static void io(Reader in, Writer out, int bufferSize) throws IOException {
if (bufferSize == -1) {
bufferSize = DEFAULT_BUFFER_SIZE >> 1;
}
char[] buffer = new char[bufferSize];
int amount;
while ((amount = in.read(buffer)) >= 0) {
out.write(buffer, 0, amount);
}
}
public static String readText(InputStream in) throws IOException {
return readText(in, null, -1);
}
public static String readText(InputStream in, String encoding) throws IOException {
return readText(in, encoding, -1);
}
public static String readText(InputStream in, String encoding, int bufferSize)
throws IOException {
Reader reader = (encoding == null) ? new InputStreamReader(in) : new InputStreamReader(in,
encoding);
return readText(reader, bufferSize);
}
public static String readText(Reader reader) throws IOException {
return readText(reader, -1);
}
public static String readText(Reader reader, int bufferSize) throws IOException {
StringWriter writer = new StringWriter();
io(reader, writer, bufferSize);
return writer.toString();
}
}

View File

@ -0,0 +1,134 @@
package site.deercloud.identityverification.Utils;
import com.alibaba.fastjson.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
public class Utils {
public static Map<String,String> ParseQueryString(String formData ) {
Map<String,String> result = new HashMap<>();
if(formData== null || formData.trim().length() == 0) {
return result;
}
final String[] items = formData.split("&");
Arrays.stream(items).forEach(item ->{
final String[] keyAndVal = item.split("=");
if( keyAndVal.length == 2) {
try{
final String key = URLDecoder.decode( keyAndVal[0],"utf8");
final String val = URLDecoder.decode( keyAndVal[1],"utf8");
result.put(key,val);
}catch (UnsupportedEncodingException ignored) {}
}
});
return result;
}
/**
* 根据玩家名字从Mojiang服务器获取UUID如果不存在则返回null
* @param name 玩家名字
*/
public static String getUUIDFromRemote(String name) throws IOException {
String url = "https://".concat("api.")
.concat("mojang.")
.concat("com/")
.concat("users/")
.concat("profiles/")
.concat("minecraft/")
.concat(name);
String reply = sendGet(url);
if (reply == null){
return null;
}
JSONObject jsonObject = JSONObject.parseObject(reply);
return jsonObject.getString("id");
}
/**
* 根据UUID从Mojiang服务器获取玩家正版信息 如果失败则返回null
* @param uuid 玩家UUID
*/
public static JSONObject getProfileFromRemote(String uuid) throws IOException {
String url = "https://".concat("sessionserver.")
.concat("mojang.")
.concat("com/")
.concat("session/")
.concat("minecraft/")
.concat("profile/")
.concat(uuid);
if (!isUUIDExist(uuid)) {
return null;
}
JSONObject jsonObject = JSONObject.parseObject(sendGet(url));
if (jsonObject == null){
return null;
}
String profile_base64 = jsonObject.getJSONArray("properties").getObject(0, JSONObject.class).getString("value");
String profile = new String(Base64.getDecoder().decode(profile_base64));
return JSONObject.parseObject(profile);
}
public static Boolean isUUIDExist(String uuid) throws IOException {
String url = "https://".concat("sessionserver.")
.concat("mojang.")
.concat("com/")
.concat("session/")
.concat("minecraft/")
.concat("profile/")
.concat(uuid);
String reply = sendGet(url);
if (reply == null){
return false;
}
return reply.contains("id");
}
public static byte[] getSkinFromRemote(String uuid) throws IOException {
JSONObject jsonObject = getProfileFromRemote(uuid);
String skin_base64 = jsonObject.getJSONObject("textures").getJSONObject("SKIN").getString("url");
return Base64.getDecoder().decode(skin_base64);
}
public static String sendGet(String url) throws IOException {
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
//默认值GET
con.setRequestMethod("GET");
//添加请求头
con.setRequestProperty("User-Agent", "Mozilla/5.0");
int responseCode = con.getResponseCode();
if (responseCode != 200) {
return null;
}
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
}
public static boolean isUUIDMatchName(String uuid, String name) throws IOException {
String reply = getUUIDFromRemote(name);
if (reply == null) {
return false;
}
return Objects.equals(reply, uuid);
}
}

View File

@ -0,0 +1,41 @@
# 服务器基本信息
Web:
Host: "127.0.0.1"
Port: 11520
# 展示在网站的标题
ServerName: "我的世界"
# 服务器核心
ImplementationName: "Bukkit"
# 服务器版本
ImplementationVersion: "1.19.2"
# 用于展示的主页地址
HomePageUrl: "https://example.com/"
# Yggdrasil服务配置
Yggdrasil:
Host: "127.0.0.1"
Port: 11521
# 允许的皮肤站域名
SkinDomains:
- "example.com"
- "example.org"
# RSA 公钥文件路径
RsaPublicKey: "id_rsa.pub"
# RSA 私钥文件路径
RsaPrivateKey: "id_rsa"
# 邮箱配置
Email:
# 邮箱服务器地址
Host: "smtp.example.com"
# 邮箱服务器端口
Port: 465
# 邮箱用户名
Username: ""
# 邮箱密码
Password: ""
# 邮箱发件人
From: ""
Debug: true

View File

@ -0,0 +1,9 @@
name: IdentityVerification
version: '${project.version}'
main: site.deercloud.identityverification.IdentityVerification
api-version: 1.13
prefix: IV
load: STARTUP
authors: [ Luming ]
description: 实名认证添加白名单、正版玩家邀请非正版注册管理。
website: https://blog.deercloud.site

View File

@ -0,0 +1,408 @@
.u-section-1 {
background-image: none;
}
.u-section-1 .u-sheet-1 {
min-height: 1108px;
}
.u-section-1 .u-text-1 {
text-transform: uppercase;
font-size: 1.25rem;
--animation-custom_in-scale: 1;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: -300px;
margin: 90px 405px 0 0;
}
.u-section-1 .u-text-2 {
font-size: 4rem;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: -300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
font-weight: 500;
margin: 42px 335px 0 0;
}
.u-section-1 .u-text-3 {
line-height: 2;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
margin: 20px 327px 0 0;
}
.u-section-1 .u-text-4 {
--animation-custom_in-scale: 1;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
margin: 35px 503px 0 0;
}
.u-section-1 .u-btn-1 {
margin-left: auto;
padding: 0;
}
.u-section-1 .u-list-1 {
margin-top: 52px;
margin-bottom: 0;
}
.u-section-1 .u-repeater-1 {
grid-template-columns: repeat(4, calc(25% - 22.5px));
min-height: 273px;
grid-gap: 30px;
}
.u-section-1 .u-list-item-1 {
background-image: none;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
box-shadow: 5px 5px 29px 0 rgba(0,0,0,0.15);
}
.u-section-1 .u-container-layout-1 {
padding: 50px 30px 30px;
}
.u-section-1 .u-icon-1 {
/* height: 59px; */
width: 80%;
background-image: none;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 0px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 180deg;
--animation-custom_in-scale: 1;
margin: 0 auto 0 0;
padding: 0;
}
.u-section-1 .u-text-5 {
font-size: 1.5rem;
font-weight: 400;
margin: 31px 0 0;
}
.u-section-1 .u-btn-2 {
font-size: 0.875rem;
background-image: none;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 600;
margin: 20px 0 0;
padding: 0;
}
.u-section-1 .u-list-item-2 {
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
box-shadow: 5px 5px 29px 0 rgba(0,0,0,0.15);
}
.u-section-1 .u-container-layout-2 {
padding: 50px 30px 30px;
}
.u-section-1 .u-icon-2 {
/* height: 59px; */
width: 80%;
background-image: none;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 0px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 180deg;
--animation-custom_in-scale: 1;
margin: 0 auto 0 0;
padding: 0;
}
.u-section-1 .u-text-6 {
font-size: 1.5rem;
font-weight: 400;
margin: 31px 0 0;
}
.u-section-1 .u-btn-3 {
font-size: 0.875rem;
background-image: none;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 600;
margin: 20px 0 0;
padding: 0;
}
.u-section-1 .u-list-item-3 {
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
box-shadow: 5px 5px 29px 0 rgba(0,0,0,0.15);
}
.u-section-1 .u-container-layout-3 {
padding: 50px 30px 30px;
}
.u-section-1 .u-icon-3 {
/* height: 59px; */
width: 80%;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 0px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 180deg;
--animation-custom_in-scale: 1;
margin: 0 auto 0 0;
padding: 0;
}
.u-section-1 .u-text-7 {
font-size: 1.5rem;
font-weight: 400;
margin: 31px 0 0;
}
.u-section-1 .u-btn-4 {
font-size: 0.875rem;
background-image: none;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 600;
margin: 20px 0 0;
padding: 0;
}
.u-section-1 .u-list-item-4 {
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
box-shadow: 5px 5px 29px 0px rgba(0,0,0,0.15);
}
.u-section-1 .u-container-layout-4 {
padding: 50px 30px 30px;
}
.u-section-1 .u-icon-4 {
/* height: 59px; */
width: 80%;
background-image: none;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 0px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 180deg;
--animation-custom_in-scale: 1;
margin: 0 auto 0 0;
padding: 0;
}
.u-section-1 .u-text-8 {
font-size: 1.5rem;
font-weight: 400;
margin: 31px 0 0;
}
.u-section-1 .u-btn-5 {
font-size: 0.875rem;
background-image: none;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 600;
margin: 20px 0 0;
padding: 0;
}
.u-section-1 .u-group-1 {
min-height: 200px;
margin-top: 52px;
margin-bottom: 60px;
height: auto;
--animation-custom_in-translate_x: 0px;
--animation-custom_in-translate_y: 300px;
--animation-custom_in-opacity: 0;
--animation-custom_in-rotate: 0deg;
--animation-custom_in-scale: 1;
}
.u-section-1 .u-container-layout-5 {
padding: 30px;
}
.u-section-1 .u-text-9 {
font-size: 3rem;
margin: 0 auto 0 0;
}
.u-section-1 .u-btn-6 {
border-style: solid;
letter-spacing: 0px;
font-size: 0.875rem;
align-self: center;
text-align: center;
font-weight: 700;
text-transform: uppercase;
margin: 15px auto 0 0;
}
@media (max-width: 1199px) {
.u-section-1 .u-text-1 {
margin-right: 205px;
}
.u-section-1 .u-text-2 {
margin-right: 135px;
}
.u-section-1 .u-text-3 {
margin-right: 127px;
}
.u-section-1 .u-text-4 {
margin-right: 303px;
}
.u-section-1 .u-repeater-1 {
min-height: 221px;
}
.u-section-1 .u-container-layout-1 {
padding-top: 40px;
padding-left: 20px;
padding-right: 20px;
}
.u-section-1 .u-group-1 {
height: auto;
}
}
@media (max-width: 991px) {
.u-section-1 .u-sheet-1 {
min-height: 1439px;
}
.u-section-1 .u-text-1 {
margin-top: 60px;
margin-right: 0;
}
.u-section-1 .u-text-2 {
margin-right: 0;
}
.u-section-1 .u-text-3 {
margin-right: 0;
}
.u-section-1 .u-text-4 {
margin-right: 83px;
}
.u-section-1 .u-repeater-1 {
grid-template-columns: repeat(2, calc(50% - 15px));
min-height: 557px;
}
.u-section-1 .u-container-layout-1 {
padding-top: 30px;
}
.u-section-1 .u-container-layout-2 {
padding-top: 30px;
}
.u-section-1 .u-container-layout-3 {
padding-top: 30px;
}
.u-section-1 .u-container-layout-4 {
padding-top: 30px;
}
.u-section-1 .u-group-1 {
min-height: 302px;
}
}
@media (max-width: 767px) {
.u-section-1 .u-text-4 {
margin-right: 0;
}
.u-section-1 .u-repeater-1 {
row-gap: 30px;
column-gap: 30px;
}
.u-section-1 .u-container-layout-2 {
padding-left: 10px;
padding-right: 10px;
}
.u-section-1 .u-container-layout-3 {
padding-left: 10px;
padding-right: 10px;
}
.u-section-1 .u-container-layout-4 {
padding-left: 10px;
padding-right: 10px;
}
.u-section-1 .u-container-layout-5 {
padding-left: 20px;
padding-right: 20px;
}
.u-section-1 .u-text-9 {
font-size: 2.25rem;
}
.u-section-1 .u-btn-6 {
margin-top: 20px;
}
}
@media (max-width: 575px) {
.u-section-1 .u-text-2 {
font-size: 2.25rem;
}
.u-section-1 .u-repeater-1 {
grid-template-columns: 100%;
}
.u-section-1 .u-container-layout-2 {
padding-left: 20px;
padding-right: 20px;
}
.u-section-1 .u-container-layout-3 {
padding-left: 20px;
padding-right: 20px;
}
.u-section-1 .u-container-layout-4 {
padding-left: 20px;
padding-right: 20px;
}
.u-section-1 .u-text-9 {
font-size: 1.875rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html style="font-size: 16px;" lang="en"><head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<meta name="keywords" content="Hello!">
<meta name="description" content="">
<title>Deer Portal</title>
<link rel="stylesheet" href="nicepage.css" media="screen">
<link rel="stylesheet" href="1234-1.css" media="screen">
<script class="u-script" type="text/javascript" src="jquery.js" defer=""></script>
<script class="u-script" type="text/javascript" src="nicepage.js" defer=""></script>
<meta name="generator" content="Nicepage 4.20.3, nicepage.com">
<link id="u-theme-google-font" rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:100,100i,200,200i,300,300i,400,400i,500,500i,600,600i,700,700i,800,800i,900,900i|Open+Sans:300,300i,400,400i,500,500i,600,600i,700,700i,800,800i">
<script type="application/ld+json">{
"@context": "http://schema.org",
"@type": "Organization",
"name": ""
}</script>
<meta name="theme-color" content="#0066ff">
<meta property="og:title" content="1234 1">
<meta property="og:type" content="website">
</head>
<body data-home-page="" data-home-page-title="" class="u-body u-xl-mode" data-lang="en">
<section class="u-align-left u-clearfix u-grey-5 u-section-1" id="carousel_0852">
<div class="u-clearfix u-sheet u-valign-middle-md u-valign-middle-sm u-valign-middle-xs u-sheet-1">
<h4 class="u-text u-text-1" data-animation-name="customAnimationIn" data-animation-duration="1500"> Deer Cloud Station Portal</h4>
<h2 class="u-text u-text-2" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="500"> 👏欢迎来到鹿鸣导航站🧭 </h2>
<p class="u-text u-text-3" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="500"> 因为一些神秘力量,为了便于访问,所有鹿站现尽量通过此门户页面跳转。各站真实地址不定期进行更新,或许一段时间后可以恢复直链访问吧。🤔 </p>
<p class="u-text u-text-4" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="500">如果跳转错误 <a href="" class="u-border-1 u-border-active-palette-2-base u-border-black u-border-hover-palette-2-base u-border-no-left u-border-no-right u-border-no-top u-bottom-left-radius-0 u-bottom-right-radius-0 u-btn u-button-link u-button-style u-none u-radius-0 u-text-body-color u-top-left-radius-0 u-top-right-radius-0 u-btn-1" target="_blank">点我反馈</a>
</p>
<div class="u-expanded-width u-list u-list-1">
<div class="u-repeater u-repeater-1">
<div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-2-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-white u-list-item-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-1"><span class="u-custom-item u-file-icon u-icon u-text-palette-2-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/blog.png" alt=""></span>
<h4 class="u-text u-text-5"> 鹿鸣的博客</h4>
<a href="https://sslbackend.deercloud.site:446" class="u-align-center u-border-2 u-border-black u-btn u-button-style u-hover-black u-none u-text-active-palette-1-base u-text-black u-text-hover-white u-btn-6" data-animation-name="" data-animation-duration="0" data-animation-delay="0" data-animation-direction="">前往 -></a>
</div>
</div>
<div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-3-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-video-cover u-white u-list-item-2" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-2"><span class="u-custom-item u-file-icon u-icon u-text-palette-5-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/mc.png" alt=""></span>
<h4 class="u-text u-text-6"> 白鹿原官网</h4>
<a href="https://sslbackend.deercloud.site:447" class="u-align-center u-border-2 u-border-black u-btn u-button-style u-hover-black u-none u-text-active-palette-1-base u-text-black u-text-hover-white u-btn-6" data-animation-name="" data-animation-duration="0" data-animation-delay="0" data-animation-direction="">前往 -></a>
</div>
</div>
<div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-5-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-video-cover u-white u-list-item-2" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-2"><span class="u-custom-item u-file-icon u-icon u-text-palette-5-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/image_bed.png" alt=""></span>
<h4 class="u-text u-text-6"> 鹿的图床站</h4>
<a href="https://sslbackend.deercloud.site:450" class="u-align-center u-border-2 u-border-black u-btn u-button-style u-hover-black u-none u-text-active-palette-1-base u-text-black u-text-hover-white u-btn-6" data-animation-name="" data-animation-duration="0" data-animation-delay="0" data-animation-direction="">前往 -></a>
</div>
</div>
<div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-4-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-video-cover u-white u-list-item-2" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-2"><span class="u-custom-item u-file-icon u-icon u-text-palette-5-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/pan.png" alt=""></span>
<h4 class="u-text u-text-6"> 云盘</h4>
<a href="https://sslbackend.deercloud.site:452" class="u-align-center u-border-2 u-border-black u-btn u-button-style u-hover-black u-none u-text-active-palette-1-base u-text-black u-text-hover-white u-btn-6" data-animation-name="" data-animation-duration="0" data-animation-delay="0" data-animation-direction="">前往 -></a>
</div>
</div>
<div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-2-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-video-cover u-white u-list-item-2" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-2"><span class="u-custom-item u-file-icon u-icon u-text-palette-5-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/bbs.png" alt=""></span>
<h4 class="u-text u-text-6"> 论坛 - (alpha)</h4>
<a href="https://sslbackend.deercloud.site:449" class="u-align-center u-border-2 u-border-black u-btn u-button-style u-hover-black u-none u-text-active-palette-1-base u-text-black u-text-hover-white u-btn-6" data-animation-name="" data-animation-duration="0" data-animation-delay="0" data-animation-direction="">前往 -></a>
</div>
</div>
<!-- <div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-3-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-video-cover u-white u-list-item-4" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-4"><span class="u-custom-item u-file-icon u-icon u-text-palette-3-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/8087597-10b05fb6.png" alt=""></span>
<h4 class="u-text u-text-8"> Application</h4>
<a href="https://nicepage.com/k/presentation-html-templates" class="u-border-1 u-border-active-grey-70 u-border-black u-border-hover-grey-70 u-border-no-left u-border-no-right u-border-no-top u-bottom-left-radius-0 u-bottom-right-radius-0 u-btn u-button-style u-custom-item u-none u-radius-0 u-text-active-palette-3-base u-text-body-color u-text-hover-palette-3-base u-top-left-radius-0 u-top-right-radius-0 u-btn-5">more</a>
</div>
</div> -->
<!-- <div class="u-align-left u-border-10 u-border-no-left u-border-no-right u-border-no-top u-border-palette-3-base u-container-style u-custom-item u-list-item u-repeater-item u-shape-rectangle u-video-cover u-white u-list-item-4" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="250">
<div class="u-container-layout u-similar-container u-valign-top u-container-layout-4"><span class="u-custom-item u-file-icon u-icon u-text-palette-3-base u-icon-1" data-animation-name="customAnimationIn" data-animation-duration="1500" data-animation-delay="750"><img src="images/8087597-10b05fb6.png" alt=""></span>
<h4 class="u-text u-text-8"> Application</h4>
<a href="https://nicepage.com/k/presentation-html-templates" class="u-border-1 u-border-active-grey-70 u-border-black u-border-hover-grey-70 u-border-no-left u-border-no-right u-border-no-top u-bottom-left-radius-0 u-bottom-right-radius-0 u-btn u-button-style u-custom-item u-none u-radius-0 u-text-active-palette-3-base u-text-body-color u-text-hover-palette-3-base u-top-left-radius-0 u-top-right-radius-0 u-btn-5">more</a>
</div>
</div> -->
</div>
</div>
<div class="u-align-left u-container-style u-expanded-width u-group u-palette-1-light-3 u-group-1" data-animation-name="customAnimationIn" data-animation-duration="1750" data-animation-delay="500">
<div class="u-container-layout u-valign-middle u-container-layout-5">
<h3 class="u-text u-text-default u-text-9"> 🪧广告位招租~</h3>
<a href="" class="u-border-1 u-border-active-grey-70 u-border-black u-border-hover-grey-70 u-border-no-left u-border-no-right u-border-no-top u-bottom-left-radius-0 u-bottom-right-radius-0 u-btn u-button-style u-custom-item u-none u-radius-0 u-text-active-palette-5-base u-text-body-color u-text-hover-palette-5-base u-top-left-radius-0 u-top-right-radius-0 u-btn-3">了解详情</a>
</div>
</div>
</div>
</section>
<section class="u-backlink u-clearfix u-grey-80">
Copyright &copy; All rights reserved | <a href="https://deercloud.site" target="_blank">成都市鹿月网络科技有限公司</a> | <a href="http://beian.miit.gov.cn/">蜀ICP备2022004667号-1</a>
</section>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

28
src/web/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

46
src/web/README.md Normal file
View File

@ -0,0 +1,46 @@
# web
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```

1
src/web/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
src/web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

5646
src/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
src/web/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "web",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"test:unit": "vitest --environment jsdom --root src/",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false"
},
"dependencies": {
"unplugin-auto-import": "^0.12.0",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.9",
"@vitejs/plugin-vue": "^3.2.0",
"@vue/test-utils": "^2.2.4",
"@vue/tsconfig": "^0.1.3",
"jsdom": "^20.0.3",
"naive-ui": "^2.34.2",
"npm-run-all": "^4.1.5",
"typescript": "~4.7.4",
"vfonts": "^0.0.3",
"vite": "^3.2.4",
"vitest": "^0.25.3",
"vue-tsc": "^1.0.9"
}
}

BIN
src/web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

39
src/web/src/App.vue Normal file
View File

@ -0,0 +1,39 @@
<template>
<header>
<RouterLink to="/">主页</RouterLink> |
<RouterLink to="/reg">注册</RouterLink>
</header>
<main>
<RouterView />
</main>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { RouterLink, RouterView } from 'vue-router'
import { NMenu, NGrid, NGridItem, NLayout, NLayoutHeader, NLayoutContent, NLayoutFooter } from 'naive-ui';
import type { MenuOption } from 'naive-ui'
</script>
<style scoped>
header {
position: fixed;
width: 100%;
top: 0;
left: 0%;
color: rgb(105, 105, 105);
padding: 1em;
padding-left: 5rem;
background-color: rgb(255, 255, 255);
}
main {
margin: auto;
padding: 1em;
position: flex;
}
</style>

View File

@ -0,0 +1,74 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,36 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 0rem;
margin-top: 5rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: flex;
margin: auto;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,71 @@
<template>
<n-form>
<n-grid :x-gap="12">
<n-grid-item span="24">
<n-form-item label="电子邮箱">
<n-input placeholder="" v-model:value="email" />
</n-form-item>
</n-grid-item>
<n-grid-item span="12">
<n-form-item label="验证码">
<n-input placeholder="" v-model:value="email_code" />
</n-form-item>
</n-grid-item>
<n-grid-item span="12">
<n-form-item label="&nbsp;">
<n-button @click="send_email_code" style="width: 100%">{{send_code_text}}</n-button>
</n-form-item>
</n-grid-item>
<n-grid-item span="24">
<n-form-item label="密码">
<n-input type="password" show-password-on="click" placeholder="" v-model:value="password" />
</n-form-item>
</n-grid-item>
<n-grid-item span="12">
<n-form-item label="邀请码">
<n-input placeholder="" v-model:value="invite_code" />
</n-form-item>
</n-grid-item>
<n-grid-item span="12">
<n-form-item label="&nbsp;">
<n-button @click="send_email_code" style="width: 100%">{{verify_invite_code_text}}</n-button>
</n-form-item>
</n-grid-item>
<n-grid-item span="24">
<n-form-item>
<n-button type="primary" @click="onSubmit" style="width: 100%;">注册外置登录</n-button>
</n-form-item>
</n-grid-item>
</n-grid>
</n-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { NButton, NForm, NFormItem, NInput, NGrid, NGridItem } from 'naive-ui'
const email = ref('')
const email_code = ref('')
const password = ref('')
const invite_code = ref('')
const send_code_text = ref('发送验证码')
const verify_invite_code_text = ref('验证邀请码')
const onSubmit = () => {
console.log(email.value, email_code.value, password.value)
}
const send_email_code = () => {
console.log('send email code')
}
</script>

View File

@ -0,0 +1,26 @@
<template>
<n-steps :current="(current as number)" :status="currentStatus">
<n-step
title="注册账号"
description="如果你有正版账号可以跳过这一步"
/>
<n-step
title="实名认证"
description="实名认证以登记服务器白名单"
/>
<n-step
title="完成!"
description="准备就绪,欢迎加入我们~"
/>
</n-steps>
</template>
<script setup lang="ts">
import { NStep, NSteps } from 'naive-ui';
import { ref } from 'vue';
const current = ref(1)
const currentStatus = ref('process')
</script>

View File

@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

12
src/web/src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import naive from 'naive-ui'
import './assets/main.css'
const app = createApp(App)
app.use(router)
app.mount('#app')

Some files were not shown because too many files have changed in this diff Show More