Added skin management, added authorization, improved UI
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package org.skinner;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
@ -122,7 +123,7 @@ public class Database {
|
||||
}
|
||||
|
||||
public static void addSkin(Skin skin) throws SQLException {
|
||||
PreparedStatement stmt = getConnection().prepareStatement(
|
||||
PreparedStatement stmt = getConnection().prepareStatement(
|
||||
"INSERT INTO skin(_hash, label, slim, png, png_old) VALUES(?, ?, ?, ?, ?);"
|
||||
);
|
||||
|
||||
@ -136,20 +137,192 @@ public class Database {
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
public static void updateSkinLabel(String hash, String label) throws SQLException {
|
||||
PreparedStatement stmt = getConnection().prepareStatement("UPDATE skin SET label = ? WHERE _hash = ?;");
|
||||
|
||||
stmt.setString(1, label);
|
||||
stmt.setString(2, hash);
|
||||
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
public static void deleteSkin(String hash) throws SQLException {
|
||||
Skin skin = getSkin(hash);
|
||||
Skin defaultSkin = ensureDefaultSkin();
|
||||
|
||||
PreparedStatement stmt = getConnection().prepareStatement("DELETE FROM skin WHERE _hash = ?;");
|
||||
|
||||
stmt.setString(1, hash);
|
||||
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
|
||||
stmt = getConnection().prepareStatement("UPDATE profile SET skin = ? WHERE skin = ?;");
|
||||
|
||||
stmt.setInt(1, defaultSkin.getId());
|
||||
stmt.setInt(2, skin.getId());
|
||||
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
public static Profile[] getProfiles() throws SQLException {
|
||||
assert(false);
|
||||
Statement stmt = getConnection().createStatement();
|
||||
ResultSet resultSet = stmt.executeQuery("SELECT profile.* FROM profile AS profile JOIN skin AS skin ON profile.skin = skin.id;");
|
||||
ResultSet resultSet = stmt.executeQuery("SELECT profile.*, skin.* FROM profile AS profile JOIN skin AS skin ON profile.skin = skin.id;");
|
||||
|
||||
List<Profile> profiles = new ArrayList<Profile>();
|
||||
while (resultSet.next()) {
|
||||
// TODO
|
||||
Skin skin = new Skin(
|
||||
resultSet.getInt("skin.id"),
|
||||
resultSet.getString("skin._hash"),
|
||||
resultSet.getString("skin.label"),
|
||||
resultSet.getBoolean("skin.slim"),
|
||||
resultSet.getBytes("skin.png"),
|
||||
resultSet.getBytes("skin.png_old")
|
||||
);
|
||||
Profile profile = new Profile(
|
||||
resultSet.getInt("profile.id"),
|
||||
resultSet.getString("profile.username"),
|
||||
resultSet.getString("profile.password"),
|
||||
resultSet.getString("profile.salt"),
|
||||
resultSet.getString("profile.uuid"),
|
||||
skin
|
||||
);
|
||||
profiles.add(profile);
|
||||
}
|
||||
stmt.close();
|
||||
|
||||
return profiles.toArray(new Profile[0]);
|
||||
}
|
||||
|
||||
public static Profile getProfileByName(String username) throws SQLException {
|
||||
PreparedStatement stmt = getConnection().prepareStatement("SELECT profile.*, skin.* FROM profile AS profile JOIN skin AS skin ON profile.skin = skin.id WHERE profile.username = ?;");
|
||||
stmt.setString(1, username);
|
||||
|
||||
ResultSet resultSet = stmt.executeQuery();
|
||||
|
||||
if (!resultSet.next())
|
||||
return null;
|
||||
|
||||
Skin skin = new Skin(
|
||||
resultSet.getInt("skin.id"),
|
||||
resultSet.getString("skin._hash"),
|
||||
resultSet.getString("skin.label"),
|
||||
resultSet.getBoolean("skin.slim"),
|
||||
resultSet.getBytes("skin.png"),
|
||||
resultSet.getBytes("skin.png_old")
|
||||
);
|
||||
Profile profile = new Profile(
|
||||
resultSet.getInt("profile.id"),
|
||||
resultSet.getString("profile.username"),
|
||||
resultSet.getString("profile.password"),
|
||||
resultSet.getString("profile.salt"),
|
||||
resultSet.getString("profile.uuid"),
|
||||
skin
|
||||
);
|
||||
|
||||
stmt.close();
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public static Profile getProfileByUUID(String uuid) throws SQLException {
|
||||
PreparedStatement stmt = getConnection().prepareStatement("SELECT profile.*, skin.* FROM profile AS profile JOIN skin AS skin ON profile.skin = skin.id WHERE profile.uuid = ?;");
|
||||
stmt.setString(1, uuid);
|
||||
|
||||
ResultSet resultSet = stmt.executeQuery();
|
||||
|
||||
if (!resultSet.next())
|
||||
return null;
|
||||
|
||||
Skin skin = new Skin(
|
||||
resultSet.getInt("skin.id"),
|
||||
resultSet.getString("skin._hash"),
|
||||
resultSet.getString("skin.label"),
|
||||
resultSet.getBoolean("skin.slim"),
|
||||
resultSet.getBytes("skin.png"),
|
||||
resultSet.getBytes("skin.png_old")
|
||||
);
|
||||
Profile profile = new Profile(
|
||||
resultSet.getInt("profile.id"),
|
||||
resultSet.getString("profile.username"),
|
||||
resultSet.getString("profile.password"),
|
||||
resultSet.getString("profile.salt"),
|
||||
resultSet.getString("profile.uuid"),
|
||||
skin
|
||||
);
|
||||
|
||||
stmt.close();
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
public static void addProfile(Profile profile) throws SQLException {
|
||||
Skin skin = ensureDefaultSkin();
|
||||
|
||||
PreparedStatement stmt = getConnection().prepareStatement(
|
||||
"INSERT INTO profile(username, password, salt, uuid, skin) VALUES(?, ?, ?, ?, ?);"
|
||||
);
|
||||
|
||||
stmt.setString(1, profile.getUsername());
|
||||
stmt.setString(2, profile.getPassword());
|
||||
stmt.setString(3, profile.getSalt());
|
||||
stmt.setString(4, profile.getUUID());
|
||||
stmt.setInt(5, skin.getId());
|
||||
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
public static void setProfileSkin(Profile profile, Skin skin) throws DatabaseException, SQLException {
|
||||
if (profile.getId() == -1)
|
||||
throw new DatabaseException("Profile provided does not have a database ID");
|
||||
if (skin.getId() == -1)
|
||||
throw new DatabaseException("Skin provided does not have a database ID");
|
||||
|
||||
PreparedStatement stmt = getConnection().prepareStatement("UPDATE profile SET skin = ? WHERE id = ?;");
|
||||
stmt.setInt(1, skin.getId());
|
||||
stmt.setInt(2, profile.getId());
|
||||
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
}
|
||||
|
||||
public static Skin getDefaultSkin() throws SQLException {
|
||||
Statement stmt = getConnection().createStatement();
|
||||
ResultSet resultSet = stmt.executeQuery("SELECT * FROM skin ORDER BY id ASC LIMIT 1;");
|
||||
if (!resultSet.next())
|
||||
return null;
|
||||
|
||||
return new Skin(
|
||||
resultSet.getInt("skin.id"),
|
||||
resultSet.getString("skin._hash"),
|
||||
resultSet.getString("skin.label"),
|
||||
resultSet.getBoolean("skin.slim"),
|
||||
resultSet.getBytes("skin.png"),
|
||||
resultSet.getBytes("skin.png_old")
|
||||
);
|
||||
}
|
||||
|
||||
private static Skin ensureDefaultSkin() throws SQLException {
|
||||
// TODO: Optimise
|
||||
Skin skin = getDefaultSkin();
|
||||
if (skin != null)
|
||||
return skin;
|
||||
try {
|
||||
// It must succeed
|
||||
addSkin(Skin.createDefaultSkin());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (SkinException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return getDefaultSkin();
|
||||
}
|
||||
|
||||
private boolean errored = false;
|
||||
private SQLException exception;
|
||||
private Connection connection;
|
||||
|
||||
40
app/src/main/java/org/skinner/DatabaseException.java
Normal file
40
app/src/main/java/org/skinner/DatabaseException.java
Normal file
@ -0,0 +1,40 @@
|
||||
package org.skinner;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class DatabaseException extends SQLException {
|
||||
|
||||
private static final long serialVersionUID = 1026873157210070017L;
|
||||
|
||||
public DatabaseException() {
|
||||
}
|
||||
|
||||
public DatabaseException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public DatabaseException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public DatabaseException(String reason, String SQLState) {
|
||||
super(reason, SQLState);
|
||||
}
|
||||
|
||||
public DatabaseException(String reason, Throwable cause) {
|
||||
super(reason, cause);
|
||||
}
|
||||
|
||||
public DatabaseException(String reason, String SQLState, int vendorCode) {
|
||||
super(reason, SQLState, vendorCode);
|
||||
}
|
||||
|
||||
public DatabaseException(String reason, String sqlState, Throwable cause) {
|
||||
super(reason, sqlState, cause);
|
||||
}
|
||||
|
||||
public DatabaseException(String reason, String sqlState, int vendorCode, Throwable cause) {
|
||||
super(reason, sqlState, vendorCode, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +1,22 @@
|
||||
package org.skinner;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.skinner.json.*;
|
||||
|
||||
public class Profile implements JSON {
|
||||
public Profile(int id, String uuid, Skin skin) {
|
||||
|
||||
public Profile(int id, String username, String password, String salt, String uuid, Skin skin) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.salt = salt;
|
||||
this.uuid = uuid;
|
||||
this.skin = skin;
|
||||
}
|
||||
@ -13,7 +25,19 @@ public class Profile implements JSON {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public String getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@ -28,8 +52,54 @@ public class Profile implements JSON {
|
||||
object.put("skin", skin.toJSON());
|
||||
return object;
|
||||
}
|
||||
|
||||
static public String generateUUID() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
static public String generateSalt() {
|
||||
byte[] salt_bytes = new byte[4];
|
||||
new Random().nextBytes(salt_bytes);
|
||||
return toHexString(salt_bytes);
|
||||
}
|
||||
|
||||
static public String generateHash(String password, String salt) throws IOException, NoSuchAlgorithmException {
|
||||
|
||||
byte[] password_bytes = password.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
bos.write(password_bytes);
|
||||
bos.write(fromHexString(salt));
|
||||
|
||||
byte[] hash_bytes = MessageDigest.getInstance("SHA-256").digest(bos.toByteArray());
|
||||
return toHexString(hash_bytes);
|
||||
}
|
||||
|
||||
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
|
||||
private static String toHexString(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] fromHexString(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private int id;
|
||||
private String username;
|
||||
private String password;
|
||||
private String salt;
|
||||
private String uuid;
|
||||
private Skin skin;
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ package org.skinner;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
import org.skinner.json.*;
|
||||
|
||||
@ -24,6 +27,10 @@ public class RestAPI extends SafeHttpHandler {
|
||||
getSkins(exchange);
|
||||
return;
|
||||
|
||||
case "/profiles":
|
||||
profiles(exchange);
|
||||
return;
|
||||
|
||||
// Template-ish routes
|
||||
default:
|
||||
if (path.startsWith("/skin/")) {
|
||||
@ -40,12 +47,42 @@ public class RestAPI extends SafeHttpHandler {
|
||||
case "/skin":
|
||||
addSkin(exchange);
|
||||
return;
|
||||
|
||||
case "/profile":
|
||||
register(exchange);
|
||||
return;
|
||||
|
||||
// Template-ish routes
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "PATCH":
|
||||
switch (path)
|
||||
{
|
||||
case "/profile":
|
||||
updateProfile(exchange, uri);
|
||||
return;
|
||||
|
||||
default:
|
||||
if (path.startsWith("/skin/")) {
|
||||
renameSkin(exchange, uri);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
switch (path)
|
||||
{
|
||||
default:
|
||||
if (path.startsWith("/skin/")) {
|
||||
deleteSkin(exchange, uri);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Headers headers = exchange.getResponseHeaders();
|
||||
headers.add("Content-Type", "text/plain");
|
||||
@ -102,13 +139,8 @@ public class RestAPI extends SafeHttpHandler {
|
||||
Skin[] skins = Database.getSkins();
|
||||
|
||||
JSONArray array = new JSONArray();
|
||||
for (Skin skin : skins) {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("hash", JSON.from(skin.getHash()));
|
||||
object.put("png", JSON.from(getRoot() + "skin/"+skin.getHash()+".png"));
|
||||
object.put("png_old", JSON.from(getRoot() + "skin/"+skin.getHash()+".png?legacy=true"));
|
||||
array.add(object);
|
||||
}
|
||||
for (Skin skin : skins)
|
||||
array.add(skinToResponseJSON(skin));
|
||||
|
||||
byte[] response = array.toString().getBytes();
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
@ -121,7 +153,7 @@ public class RestAPI extends SafeHttpHandler {
|
||||
MultipartForm form = new MultipartForm(exchange);
|
||||
|
||||
if (!form.contains("skin")) {
|
||||
fail(exchange, "Expected \"skin\" field not provided");
|
||||
text(exchange, 400, "Expected \"skin\" field not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -131,4 +163,116 @@ public class RestAPI extends SafeHttpHandler {
|
||||
ok(exchange);
|
||||
}
|
||||
|
||||
protected void renameSkin(HttpExchange exchange, URI uri) throws Exception {
|
||||
String[] pathParts = uri.getPath().split("/");
|
||||
String hash = pathParts[pathParts.length-1];
|
||||
|
||||
String label = new String(exchange.getRequestBody().readAllBytes());
|
||||
|
||||
Database.updateSkinLabel(hash, label);
|
||||
ok(exchange);
|
||||
}
|
||||
|
||||
protected void deleteSkin(HttpExchange exchange, URI uri) throws Exception {
|
||||
String[] pathParts = uri.getPath().split("/");
|
||||
String hash = pathParts[pathParts.length-1];
|
||||
|
||||
Database.deleteSkin(hash);
|
||||
ok(exchange);
|
||||
}
|
||||
|
||||
protected void profiles(HttpExchange exchange) throws Exception {
|
||||
Profile[] profiles = Database.getProfiles();
|
||||
|
||||
JSONArray array = new JSONArray();
|
||||
for (Profile profile : profiles)
|
||||
array.add(profileToResponseJSON(profile));
|
||||
|
||||
byte[] response = array.toString().getBytes();
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, response.length);
|
||||
exchange.getResponseBody().write(response);
|
||||
exchange.close();
|
||||
}
|
||||
|
||||
protected void register(HttpExchange exchange) throws Exception {
|
||||
MultipartForm form = new MultipartForm(exchange);
|
||||
|
||||
if (!form.contains("username")) {
|
||||
text(exchange, 400, "Expected \"username\" field not provided");
|
||||
return;
|
||||
}
|
||||
if (!form.contains("password")) {
|
||||
text(exchange, 400, "Expected \"password\" field not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
String username = form.getString("username");
|
||||
String password = form.getString("password");
|
||||
|
||||
String salt = Profile.generateSalt();
|
||||
String hash = Profile.generateHash(password, salt);
|
||||
String uuid = Profile.generateUUID();
|
||||
|
||||
Database.addProfile(new Profile(
|
||||
-1,
|
||||
username,
|
||||
hash,
|
||||
salt,
|
||||
uuid,
|
||||
null
|
||||
));
|
||||
ok(exchange);
|
||||
}
|
||||
|
||||
protected void updateProfile(HttpExchange exchange, URI uri) throws Exception {
|
||||
Headers headers = exchange.getRequestHeaders();
|
||||
if (!headers.containsKey("Authorization")) {
|
||||
String response = "Unauthorized";
|
||||
exchange.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"Skinner\"");
|
||||
exchange.sendResponseHeaders(401, response.length());
|
||||
exchange.getResponseBody().write(response.getBytes());
|
||||
exchange.close();
|
||||
return;
|
||||
}
|
||||
String auth = headers.getFirst("Authorization");
|
||||
String[] authParts = auth.trim().split(" ");
|
||||
|
||||
if (!authParts[0].equals("Basic")) {
|
||||
text(exchange, 401, "Invalid authorization method");
|
||||
return;
|
||||
}
|
||||
|
||||
String[] cerd = new String(Base64.getDecoder().decode(authParts[1])).split(":");
|
||||
Profile profile = Database.getProfileByName(cerd[0]);
|
||||
|
||||
if (profile == null || !Profile.generateHash(cerd[1], profile.getSalt()).equals(profile.getPassword())) {
|
||||
text(exchange, 401, "Failed to authorize");
|
||||
return;
|
||||
}
|
||||
|
||||
Query query = new Query(uri.getQuery());
|
||||
|
||||
if (query.containsKey("skin"))
|
||||
Database.setProfileSkin(profile, Database.getSkin(query.get("skin")));
|
||||
|
||||
ok(exchange);
|
||||
}
|
||||
|
||||
private JSONObject skinToResponseJSON(Skin skin) {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("hash", JSON.from(skin.getHash()));
|
||||
object.put("png", JSON.from(getRoot() + "skin/"+skin.getHash()+".png"));
|
||||
object.put("png_old", JSON.from(getRoot() + "skin/"+skin.getHash()+".png?legacy=true"));
|
||||
return object;
|
||||
}
|
||||
|
||||
private JSONObject profileToResponseJSON(Profile profile) {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("username", JSON.from(profile.getUsername()));
|
||||
object.put("uuid", JSON.from(profile.getUUID()));
|
||||
object.put("skin", skinToResponseJSON(profile.getSkin()));
|
||||
return object;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -153,6 +153,10 @@ public class Skin implements JSON {
|
||||
return new Skin(-1, hash, label, slim, png, png_old);
|
||||
}
|
||||
|
||||
public static Skin createDefaultSkin() throws SkinException, IOException, NoSuchAlgorithmException {
|
||||
return loadFromImage(WebApp.class.getResourceAsStream("/steve.png"), "Steve");
|
||||
}
|
||||
|
||||
private static boolean matchAreaRGB(BufferedImage image, int rgb, int x, int y, int w, int h) {
|
||||
int endX = x + w;
|
||||
int endY = y + h;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS skin(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
_hash VARCHAR(64) UNIQUE NOT NULL,
|
||||
_hash CHAR(64) UNIQUE NOT NULL,
|
||||
label TEXT,
|
||||
slim BOOLEAN NOT NULL,
|
||||
png BLOB NOT NULL,
|
||||
@ -10,7 +10,9 @@ CREATE TABLE IF NOT EXISTS skin(
|
||||
CREATE TABLE IF NOT EXISTS profile(
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
uuid VARCHAR(36) UNIQUE NOT NULL,
|
||||
password CHAR(64) NOT NULL,
|
||||
salt CHAR(8) NOT NULL,
|
||||
uuid CHAR(32) UNIQUE NOT NULL,
|
||||
skin INTEGER NOT NULL,
|
||||
FOREIGN KEY (skin) REFERENCES skin(id)
|
||||
);
|
||||
|
||||
BIN
app/src/main/resources/steve.png
Normal file
BIN
app/src/main/resources/steve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@ -13,4 +13,23 @@ api = {
|
||||
data.append("label", label);
|
||||
await fetch("api/skin", {"method": "POST", body: data});
|
||||
},
|
||||
renameSkin: async function(skin_hash, label) {
|
||||
await fetch(`api/skin/${skin_hash}`, {method: "PATCH", body: label});
|
||||
},
|
||||
deleteSkin: async function(skin_hash) {
|
||||
await fetch(`api/skin/${skin_hash}`, {method: "DELETE"});
|
||||
},
|
||||
changeSkin: async function(username, password, skin_hash) {
|
||||
let res = await fetch(
|
||||
`api/profile?skin=${encodeURI(skin_hash)}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Authorization": "Basic " + btoa(`${username}:${password}`),
|
||||
},
|
||||
}
|
||||
)
|
||||
if (res.status != 200)
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
};
|
||||
@ -34,7 +34,7 @@
|
||||
left: 0;
|
||||
margin: 0;
|
||||
background-color: var(--bg);
|
||||
transform: translate(50%, 50%);
|
||||
transform: translate(50%, 25%);
|
||||
min-width: 50vw;
|
||||
aspect-ratio: 1.5/1;
|
||||
}
|
||||
@ -69,19 +69,24 @@
|
||||
margin-left: 1.666%;
|
||||
margin-right: 1.666%;
|
||||
}
|
||||
.skin-img
|
||||
{
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<title>Skinner</title>
|
||||
</head>
|
||||
<body x-data="{add_dialog: false, blur: false, skins: api.getSkins()}">
|
||||
<body x-data="{skin_dialog: false, add_dialog: false, skins: api.getSkins(), blur: false}" x-effect="blur = skin_dialog || add_dialog">
|
||||
<main x-data="{search: ''}" x-bind:style="blur ? 'filter: blur(10px);' : ''">
|
||||
<h1>Skinner</h1>
|
||||
<div class="flex-container">
|
||||
<input class="flex" type="search" placeholder="Search skins..." x-model="search">
|
||||
<button @click="add_dialog = true; blur = true;">Add new</button>
|
||||
<button @click="add_dialog = true;">Add new</button>
|
||||
</div>
|
||||
<div id="skin-cards">
|
||||
<template x-for="skin in skins">
|
||||
<article class="skin-card" x-show="(skin.label || '').toLowerCase().includes(search.toLowerCase())">
|
||||
<article class="skin-card" x-show="(skin.label || '').toLowerCase().includes(search.toLowerCase())" @click="skin_dialog = skin">
|
||||
<img x-bind:src="await renderSkin(skin.png)">
|
||||
<big x-text="skin.label || 'Unnamed'"></big>
|
||||
</article>
|
||||
@ -90,13 +95,33 @@
|
||||
</main>
|
||||
<div class="screenblock" x-show="blur"><!-- Block to prevent mouse clicks --></div>
|
||||
<dialog x-bind:open="add_dialog">
|
||||
<form method="POST" action="api/skin" enctype="multipart/form-data" x-data="{files: null, label: '', processing: false}" @submit.prevent="processing = true; api.addSkin(files, label).then(async ()=>{skins = await api.getSkins(); blur = false; add_dialog = false; processing = false;})">
|
||||
<form method="POST" action="api/skin" enctype="multipart/form-data" x-data="{files: null, label: '', processing: false}" @submit.prevent="processing = true; api.addSkin(files, label).then(async ()=>{skins = await api.getSkins(); add_dialog = false; processing = false;})">
|
||||
<h1>Add Skin</h1><br>
|
||||
<input type="text" placeholder="Skin name" x-model="label">
|
||||
<input type="file" accept="image/png" @change="files = $el.files[0]" required><br><br><br>
|
||||
<input type="submit" value="Add new skin" x-bind:disabled="processing">
|
||||
<button @click="blur = false; add_dialog = false;" x-bind:disabled="processing">Cancel</button>
|
||||
<button @click="add_dialog = false" x-bind:disabled="processing">Cancel</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog x-bind:open="skin_dialog" x-data="{username: '', password: '', label: '', processing: false}" x-effect="label = skin_dialog ? skin_dialog.label : ''">
|
||||
<div style="width: 100%; height: 100%; text-align: center;">
|
||||
<img class="skin-img" x-bind:src="skin_dialog ? await renderSkin(skin_dialog.png) : ''">
|
||||
</div>
|
||||
<div style="display: grid;" x-data="{delete_confirm: false}" x-effect="delete_confirm = skin_dialog && delete_confirm">
|
||||
<input style="grid-area: 1/1/2/5;" x-model="label" placeholder="Unnamed" x-bind:disabled="processing">
|
||||
<button style="grid-area: 1/5;" x-bind:disabled="processing" @click="processing = true; await api.renameSkin(skin_dialog.hash, label).then(async ()=>{skins = await api.getSkins();}); processing = false;">Rename</button>
|
||||
<input style="grid-area: 2/1/3/3;" x-model="username" placeholder="Username" type="text" x-bind:disabled="processing">
|
||||
<input style="grid-area: 2/3/3/5;" x-model="password" placeholder="Password" type="password" x-bind:disabled="processing">
|
||||
<button style="grid-area: 2/5;" x-bind:disabled="processing" @click="processing = true; await api.changeSkin(username, password, skin_dialog.hash).then(async ()=>{skins = await api.getSkins();}).catch(()=>{alert('Invalid credentials');}); processing = false;">Apply</button>
|
||||
<button style="grid-area: 3/1;" x-bind:disabled="!delete_confirm || processing" @click="processing = true; await api.deleteSkin(skin_dialog.hash).then(async ()=>{skins = await api.getSkins();}); processing = false; skin_dialog = false; delete_confirm = false;">Delete</button>
|
||||
<div style="grid-area: 3/2; align-content: center;">
|
||||
<label>
|
||||
<input type="checkbox" x-bind:checked="delete_confirm" @click="delete_confirm = $el.checked" x-bind:disabled="processing">
|
||||
Confirm
|
||||
</label>
|
||||
</div>
|
||||
<button style="grid-area: 3/5;" x-bind:disabled="processing" @click="skin_dialog = false">Close</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user