Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
f5372c153c | |||
509c60e632 | |||
247b2a6d6f | |||
c0e2489828 | |||
980eaf05ba | |||
ed823e9391 | |||
b8cd5b299d | |||
|
768d4487fb | ||
|
8325b98b8b | ||
|
256b7d3d34 | ||
|
460d454f08 | ||
|
bd0a2e907d | ||
|
968602ac0d | ||
|
409f0ce80a | ||
11ba593439 | |||
|
ce90040ab7 | ||
50e64a00c5 | |||
0eb8ef6824 | |||
56ca2f5fd5 | |||
|
7c6e3b470b | ||
16968427b3 | |||
8d97e0efa4 | |||
df004e59e2 | |||
b6f43a6510 | |||
e1a829664a | |||
c6f968851a | |||
f32a92fe54 | |||
49b8fe39a5 | |||
9215ac87b0 | |||
d7dc635ffb | |||
ab98d752c8 | |||
3e16f212da | |||
e391d3e48e | |||
67a88d3366 | |||
97aea0dafc | |||
be0825bf15 | |||
ce6abbdf55 | |||
f95296db06 | |||
5c7c7e9f68 | |||
1eda57a7e3 | |||
57b5c4e05d | |||
2e1e2f5127 |
@ -1,4 +1,4 @@
|
|||||||
name: Publish Image
|
name: Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -10,16 +10,18 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
arch: [ "ubuntu-latest" ]
|
arch: [ "ubuntu-latest" ]
|
||||||
git-version: [ "2.38.4" ]
|
git-version: [ "2.38.4" ]
|
||||||
|
|
||||||
runs-on: ${{ matrix.arch }}
|
runs-on: ${{ matrix.arch }}
|
||||||
container: git.rainnny.club/rainnny/gitea-runner:node-18
|
container: fascinated/docker-images:nodejs_20
|
||||||
steps:
|
steps:
|
||||||
|
# Checkout the branch
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://github.com/actions/checkout@v3
|
uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
|
# Setup Docker BuildX
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: https://github.com/docker/setup-buildx-action@v2
|
uses: https://github.com/docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
# Login to Docker
|
||||||
- name: Login to Repo
|
- name: Login to Repo
|
||||||
uses: https://github.com/docker/login-action@v2
|
uses: https://github.com/docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
@ -27,6 +29,7 @@ jobs:
|
|||||||
username: ${{ secrets.REPO_USERNAME }}
|
username: ${{ secrets.REPO_USERNAME }}
|
||||||
password: ${{ secrets.REPO_TOKEN }}
|
password: ${{ secrets.REPO_TOKEN }}
|
||||||
|
|
||||||
|
# Build & Push to Docker
|
||||||
- name: Build and Push
|
- name: Build and Push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,4 +24,5 @@ com_crashlytics_export_strings.xml
|
|||||||
crashlytics.properties
|
crashlytics.properties
|
||||||
crashlytics-build.properties
|
crashlytics-build.properties
|
||||||
fabric.properties
|
fabric.properties
|
||||||
git.properties
|
git.properties
|
||||||
|
CLI
|
@ -14,7 +14,7 @@ COPY src ./src
|
|||||||
RUN mvn clean package -T12
|
RUN mvn clean package -T12
|
||||||
|
|
||||||
# Stage 2: Running
|
# Stage 2: Running
|
||||||
FROM openjdk:17.0.1-jdk-slim
|
FROM openjdk:17.0.2-jdk-slim
|
||||||
|
|
||||||
# Set the work dir inside the container
|
# Set the work dir inside the container
|
||||||
WORKDIR /usr/local/app
|
WORKDIR /usr/local/app
|
||||||
|
2
Example-Java/README.md
Normal file
2
Example-Java/README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Java Example
|
||||||
|
This is the example of how to interact with the license server from within Java.
|
@ -20,7 +20,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.13.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>${java.version}</source>
|
<source>${java.version}</source>
|
||||||
<target>${java.version}</target>
|
<target>${java.version}</target>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>3.1.0</version>
|
<version>3.6.0</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.28</version>
|
<version>1.18.30</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -64,7 +64,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.oshi</groupId>
|
<groupId>com.github.oshi</groupId>
|
||||||
<artifactId>oshi-core</artifactId>
|
<artifactId>oshi-core</artifactId>
|
||||||
<version>6.4.2</version>
|
<version>6.6.5</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -72,7 +72,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
<artifactId>okhttp</artifactId>
|
<artifactId>okhttp</artifactId>
|
||||||
<version>4.11.0</version>
|
<version>4.12.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
@ -1,13 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.example;
|
package me.braydon.example;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.*;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.ToString;
|
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
import oshi.SystemInfo;
|
import oshi.SystemInfo;
|
||||||
import oshi.hardware.CentralProcessor;
|
import oshi.hardware.CentralProcessor;
|
||||||
@ -15,9 +17,18 @@ import oshi.hardware.ComputerSystem;
|
|||||||
import oshi.hardware.HardwareAbstractionLayer;
|
import oshi.hardware.HardwareAbstractionLayer;
|
||||||
import oshi.software.os.OperatingSystem;
|
import oshi.software.os.OperatingSystem;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -32,51 +43,93 @@ import java.util.Map;
|
|||||||
* @author Braydon
|
* @author Braydon
|
||||||
* @see <a href="https://git.rainnny.club/Rainnny/LicenseServer">License Server</a>
|
* @see <a href="https://git.rainnny.club/Rainnny/LicenseServer">License Server</a>
|
||||||
*/
|
*/
|
||||||
public final class LicenseExample {
|
public final class LicenseClient {
|
||||||
|
private static final String ALGORITHM = "RSA"; // The crypto algorithm to use
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The endpoint to use for downloading the {@link PublicKey}.
|
||||||
|
*/
|
||||||
|
private static final String PUBLIC_KEY_ENDPOINT = "/crypto/pub";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The endpoint to check licenses at.
|
* The endpoint to check licenses at.
|
||||||
*/
|
*/
|
||||||
private static final String CHECK_ENDPOINT = "http://localhost:7500/check";
|
private static final String CHECK_ENDPOINT = "/check";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link Gson} instance to use.
|
* The {@link Gson} instance to use.
|
||||||
*/
|
*/
|
||||||
private static final Gson GSON = new GsonBuilder()
|
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
|
||||||
.serializeNulls()
|
|
||||||
.create();
|
/**
|
||||||
|
* The URL of the license server to make requests to.
|
||||||
|
*/
|
||||||
|
@NonNull private final String appUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The product to use for client.
|
||||||
|
*/
|
||||||
|
@NonNull private final String product;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link OkHttpClient} to use for requests.
|
||||||
|
*/
|
||||||
|
@NonNull private final OkHttpClient httpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PublicKey} to use for encryption.
|
||||||
|
*/
|
||||||
|
@NonNull private final PublicKey publicKey;
|
||||||
|
|
||||||
|
public LicenseClient(@NonNull String appUrl, @NonNull String product, @NonNull File publicKeyFile) {
|
||||||
|
this.appUrl = appUrl;
|
||||||
|
this.product = product;
|
||||||
|
httpClient = new OkHttpClient(); // Create a new http client
|
||||||
|
publicKey = fetchPublicKey(publicKeyFile); // Fetch our public key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the public key from the given bytes.
|
||||||
|
*
|
||||||
|
* @param bytes the bytes of the public key
|
||||||
|
* @return the public key
|
||||||
|
* @see PrivateKey for public key
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
private static PublicKey readPublicKey(byte[] bytes) {
|
||||||
|
return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the license with the given
|
* Check the license with the given
|
||||||
* key for the given product.
|
* key for the given product.
|
||||||
*
|
*
|
||||||
* @param key the key to check
|
* @param key the key to check
|
||||||
* @param product the product the key belongs to
|
|
||||||
* @return the license response
|
* @return the license response
|
||||||
* @see LicenseResponse for response
|
* @see LicenseResponse for response
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static LicenseResponse check(@NonNull String key, @NonNull String product) {
|
public LicenseResponse check(@NonNull String key) {
|
||||||
String hardwareId = getHardwareId(); // Get the hardware id of the machine
|
String hardwareId = getHardwareId(); // Get the hardware id of the machine
|
||||||
|
|
||||||
// Build the json body
|
// Build the json body
|
||||||
Map<String, Object> body = new HashMap<>();
|
Map<String, Object> body = new HashMap<>();
|
||||||
body.put("key", key);
|
body.put("key", encrypt(key));
|
||||||
body.put("product", product);
|
body.put("product", product);
|
||||||
body.put("hwid", hardwareId);
|
body.put("hwid", encrypt(hardwareId));
|
||||||
String bodyJson = GSON.toJson(body); // The json body
|
String bodyJson = GSON.toJson(body); // The json body
|
||||||
|
|
||||||
OkHttpClient client = new OkHttpClient(); // Create a new http client
|
|
||||||
MediaType mediaType = MediaType.parse("application/json"); // Ensure the media type is json
|
MediaType mediaType = MediaType.parse("application/json"); // Ensure the media type is json
|
||||||
RequestBody requestBody = RequestBody.create(bodyJson, mediaType); // Build the request body
|
RequestBody requestBody = RequestBody.create(mediaType, bodyJson); // Build the request body
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(CHECK_ENDPOINT)
|
.url(appUrl + CHECK_ENDPOINT)
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
.build(); // Build the POST request
|
.build(); // Build the POST request
|
||||||
|
|
||||||
Response response = null; // The response of the request
|
Response response = null; // The response of the request
|
||||||
int responseCode = -1; // The response code of the request
|
int responseCode = -1; // The response code of the request
|
||||||
try { // Attempt to execute the request
|
try { // Attempt to execute the request
|
||||||
response = client.newCall(request).execute();
|
response = httpClient.newCall(request).execute();
|
||||||
responseCode = response.code();
|
responseCode = response.code();
|
||||||
|
|
||||||
// If the response is successful, we can parse the response
|
// If the response is successful, we can parse the response
|
||||||
@ -88,6 +141,8 @@ public final class LicenseExample {
|
|||||||
JsonElement description = json.get("description");
|
JsonElement description = json.get("description");
|
||||||
JsonElement ownerSnowflake = json.get("ownerSnowflake");
|
JsonElement ownerSnowflake = json.get("ownerSnowflake");
|
||||||
JsonElement ownerName = json.get("ownerName");
|
JsonElement ownerName = json.get("ownerName");
|
||||||
|
JsonElement plan = json.get("plan");
|
||||||
|
JsonElement latestVersion = json.get("latestVersion");
|
||||||
|
|
||||||
// Parsing the expiration date if we have one
|
// Parsing the expiration date if we have one
|
||||||
JsonElement expires = json.get("expires");
|
JsonElement expires = json.get("expires");
|
||||||
@ -102,6 +157,8 @@ public final class LicenseExample {
|
|||||||
description.isJsonNull() ? null : description.getAsString(),
|
description.isJsonNull() ? null : description.getAsString(),
|
||||||
ownerSnowflake.isJsonNull() ? -1 : ownerSnowflake.getAsLong(),
|
ownerSnowflake.isJsonNull() ? -1 : ownerSnowflake.getAsLong(),
|
||||||
ownerName.isJsonNull() ? null : ownerName.getAsString(),
|
ownerName.isJsonNull() ? null : ownerName.getAsString(),
|
||||||
|
plan.getAsString(),
|
||||||
|
latestVersion.getAsString(),
|
||||||
expires.isJsonNull() ? null : expiresDate
|
expires.isJsonNull() ? null : expiresDate
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -127,6 +184,43 @@ public final class LicenseExample {
|
|||||||
return new LicenseResponse(responseCode, "An unknown error occurred");
|
return new LicenseResponse(responseCode, "An unknown error occurred");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the public key.
|
||||||
|
* <p>
|
||||||
|
* If the public key is not already present, we
|
||||||
|
* fetch it from the server. Otherwise, the public
|
||||||
|
* key is loaded from the file.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param publicKeyFile the public key file
|
||||||
|
* @return the public key
|
||||||
|
* @see PublicKey for public key
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
private PublicKey fetchPublicKey(@NonNull File publicKeyFile) {
|
||||||
|
byte[] publicKey;
|
||||||
|
if (publicKeyFile.exists()) { // Public key exists, use it
|
||||||
|
publicKey = Files.readAllBytes(publicKeyFile.toPath());
|
||||||
|
} else {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(appUrl + PUBLIC_KEY_ENDPOINT)
|
||||||
|
.build(); // Build the GET request
|
||||||
|
@Cleanup Response response = httpClient.newCall(request).execute(); // Make the request
|
||||||
|
if (!response.isSuccessful()) { // Response wasn't successful
|
||||||
|
throw new IOException("Failed to download the public key, got response " + response.code());
|
||||||
|
}
|
||||||
|
ResponseBody body = response.body(); // Get the response body
|
||||||
|
assert body != null; // We need a response body
|
||||||
|
publicKey = body.bytes(); // Read our public key
|
||||||
|
|
||||||
|
// Write the response to the public key file
|
||||||
|
try (FileOutputStream outputStream = new FileOutputStream(publicKeyFile)) {
|
||||||
|
outputStream.write(publicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readPublicKey(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the unique hardware
|
* Get the unique hardware
|
||||||
* identifier of this machine.
|
* identifier of this machine.
|
||||||
@ -134,7 +228,7 @@ public final class LicenseExample {
|
|||||||
* @return the hardware id
|
* @return the hardware id
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private static String getHardwareId() {
|
private String getHardwareId() {
|
||||||
SystemInfo systemInfo = new SystemInfo();
|
SystemInfo systemInfo = new SystemInfo();
|
||||||
OperatingSystem operatingSystem = systemInfo.getOperatingSystem();
|
OperatingSystem operatingSystem = systemInfo.getOperatingSystem();
|
||||||
HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware();
|
HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware();
|
||||||
@ -155,9 +249,25 @@ public final class LicenseExample {
|
|||||||
+ String.format("%08x", processorIdentifier.hashCode()) + "-" + processors;
|
+ String.format("%08x", processorIdentifier.hashCode()) + "-" + processors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
/**
|
||||||
@Getter
|
* Encrypt the given input.
|
||||||
@ToString
|
*
|
||||||
|
* @param input the encrypted input
|
||||||
|
* @return the encrypted result
|
||||||
|
*/
|
||||||
|
@SneakyThrows @NonNull
|
||||||
|
private String encrypt(@NonNull String input) {
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM); // Create our cipher
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey); // Set our mode and public key
|
||||||
|
return Base64.getEncoder().encodeToString(cipher.doFinal(input.getBytes())); // Return our encrypted result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response of a license check.
|
||||||
|
*
|
||||||
|
* @see #check(String)
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
public static class LicenseResponse {
|
public static class LicenseResponse {
|
||||||
/**
|
/**
|
||||||
* The status code of the response.
|
* The status code of the response.
|
||||||
@ -186,6 +296,16 @@ public final class LicenseExample {
|
|||||||
*/
|
*/
|
||||||
private String ownerName;
|
private String ownerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plan for this license.
|
||||||
|
*/
|
||||||
|
@NonNull private String plan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest version of the product this license is for.
|
||||||
|
*/
|
||||||
|
@NonNull private String latestVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The optional expiration {@link Date} of the license.
|
* The optional expiration {@link Date} of the license.
|
||||||
*/
|
*/
|
@ -1,11 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.example;
|
package me.braydon.example;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Braydon
|
* @author Braydon
|
||||||
*/
|
*/
|
||||||
public final class Main {
|
public final class Main {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
LicenseExample.LicenseResponse response = LicenseExample.check("XXXX-XXXX-XXXX-XXXX", "Example");
|
LicenseClient client = new LicenseClient("http://localhost:7500", "Example", new File("public.key")); // Create the client
|
||||||
|
LicenseClient.LicenseResponse response = client.check("XXXX-XXXX-XXXX-XXXX"); // Check our license
|
||||||
if (!response.isValid()) { // License isn't valid
|
if (!response.isValid()) { // License isn't valid
|
||||||
System.err.println("Invalid license: " + response.getError());
|
System.err.println("Invalid license: " + response.getError());
|
||||||
return;
|
return;
|
||||||
@ -13,7 +21,7 @@ public final class Main {
|
|||||||
// License is valid
|
// License is valid
|
||||||
System.out.println("License is valid!");
|
System.out.println("License is valid!");
|
||||||
if (response.getOwnerName() != null) {
|
if (response.getOwnerName() != null) {
|
||||||
System.out.println("Welcome " + response.getOwnerName() + "!");
|
System.out.println("Welcome " + response.getOwnerName() + "! Your plan is " + response.getPlan() + " and the latest version is " + response.getLatestVersion());
|
||||||
}
|
}
|
||||||
if (response.getDescription() != null) {
|
if (response.getDescription() != null) {
|
||||||
System.out.println("Description: " + response.getDescription()); // License description
|
System.out.println("Description: " + response.getDescription()); // License description
|
3
Frontend/.eslintrc.json
Normal file
3
Frontend/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
17
Frontend/.gitignore
vendored
Normal file
17
Frontend/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
/coverage
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
/build
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.env
|
||||||
|
.vercel
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
.directory
|
36
Frontend/README.md
Normal file
36
Frontend/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
16
Frontend/components.json
Normal file
16
Frontend/components.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/libs"
|
||||||
|
}
|
||||||
|
}
|
4
Frontend/next.config.js
Normal file
4
Frontend/next.config.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
42
Frontend/package.json
Normal file
42
Frontend/package.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "license-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@8.7.1",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Braydon",
|
||||||
|
"email": "braydonrainnny@gmail.com",
|
||||||
|
"url": "https://rainnny.club"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-context-menu": "^2.1.5",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"next": "14.0.3",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"tailwind-merge": "^2.0.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10.0.1",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.0.3",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.3.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
3204
Frontend/pnpm-lock.yaml
generated
Normal file
3204
Frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
Frontend/postcss.config.js
Normal file
6
Frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
Frontend/public/logo.png
Normal file
BIN
Frontend/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
34
Frontend/src/app/(routes)/layout.tsx
Normal file
34
Frontend/src/app/(routes)/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import "../globals.css";
|
||||||
|
|
||||||
|
import { Inter as FontSans } from "next/font/google";
|
||||||
|
import { cn } from "../libs/libs";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fontSans = FontSans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={cn(
|
||||||
|
"min-h-screen bg-background font-sans antialiased",
|
||||||
|
fontSans.variable
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
26
Frontend/src/app/(routes)/page.tsx
Normal file
26
Frontend/src/app/(routes)/page.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Button } from "../components/ui/button";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "../components/ui/context-menu";
|
||||||
|
|
||||||
|
const Home = (): JSX.Element => (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuTrigger>
|
||||||
|
<Button>Click Me</Button>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem>Profile</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Billing</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Team</ContextMenuItem>
|
||||||
|
<ContextMenuItem>Subscription</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default Home;
|
56
Frontend/src/app/components/ui/button.tsx
Normal file
56
Frontend/src/app/components/ui/button.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/libs"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
200
Frontend/src/app/components/ui/context-menu.tsx
Normal file
200
Frontend/src/app/components/ui/context-menu.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/libs"
|
||||||
|
|
||||||
|
const ContextMenu = ContextMenuPrimitive.Root
|
||||||
|
|
||||||
|
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const ContextMenuGroup = ContextMenuPrimitive.Group
|
||||||
|
|
||||||
|
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const ContextMenuSub = ContextMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const ContextMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const ContextMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const ContextMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const ContextMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const ContextMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
ContextMenuCheckboxItem.displayName =
|
||||||
|
ContextMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const ContextMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const ContextMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const ContextMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const ContextMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
}
|
BIN
Frontend/src/app/favicon.ico
Normal file
BIN
Frontend/src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
76
Frontend/src/app/globals.css
Normal file
76
Frontend/src/app/globals.css
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
6
Frontend/src/app/libs/libs.ts
Normal file
6
Frontend/src/app/libs/libs.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
76
Frontend/tailwind.config.ts
Normal file
76
Frontend/tailwind.config.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const { fontFamily } = require("tailwindcss/defaultTheme");
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: ["./src/app/**/*.{ts,tsx}"],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
29
Frontend/tsconfig.json
Normal file
29
Frontend/tsconfig.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/app/*"],
|
||||||
|
"@/components/*": ["./src/app/components/*"],
|
||||||
|
"@/libs": ["./src/app/libs/libs.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
15
README.md
15
README.md
@ -1,5 +1,8 @@
|
|||||||
# LicenseServer
|
[![Discord](https://discord.com/api/guilds/827863713855176755/widget.png)](https://discord.gg/p9gzFE2bc6)
|
||||||
|
![Wakatime Hours](https://wakatime.rainnny.club/api/badge/Rainnny/interval:any/project:LicenseServer)
|
||||||
|
[![Download](https://img.shields.io/badge/Download-Releases-darkgreen.svg)](https://git.rainnny.club/Rainnny/LicenseServer/releases)
|
||||||
|
|
||||||
|
# LicenseServer
|
||||||
A simple open-source licensing server for your products.
|
A simple open-source licensing server for your products.
|
||||||
|
|
||||||
## Discord Preview
|
## Discord Preview
|
||||||
@ -18,11 +21,11 @@ POST /check
|
|||||||
|
|
||||||
#### Body
|
#### Body
|
||||||
|
|
||||||
| Key | Type | Description |
|
| Key | Type | Description |
|
||||||
|:----------|:---------|:-----------------------------------------------|
|
|:----------|:---------|:----------------------------------------------------------------|
|
||||||
| `key` | `string` | **Required**. Your license key |
|
| `key` | `string` | **Required**. Your base64 encrypted license key |
|
||||||
| `product` | `string` | **Required**. The product the license is for |
|
| `product` | `string` | **Required**. The product the license is for |
|
||||||
| `hwid` | `string` | **Required**. The hardware id of the requester |
|
| `hwid` | `string` | **Required**. The base64 encrypted hardware id of the requester |
|
||||||
|
|
||||||
#### Response
|
#### Response
|
||||||
|
|
||||||
|
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: git.rainnny.club/rainnny/licenseserver:latest
|
||||||
|
volumes:
|
||||||
|
- ./data/application.yml:/usr/local/app/application.yml
|
||||||
|
ports:
|
||||||
|
- "7500:7500"
|
10
pom.xml
10
pom.xml
@ -6,13 +6,13 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.1.0</version>
|
<version>3.4.0</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>me.braydon</groupId>
|
<groupId>me.braydon</groupId>
|
||||||
<artifactId>LicenseServer</artifactId>
|
<artifactId>LicenseServer</artifactId>
|
||||||
<version>1.0.3</version>
|
<version>1.0.5</version>
|
||||||
<description>A simple open-source licensing server for your products.</description>
|
<description>A simple open-source licensing server for your products.</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.28</version>
|
<version>1.18.30</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.dv8tion</groupId>
|
<groupId>net.dv8tion</groupId>
|
||||||
<artifactId>JDA</artifactId>
|
<artifactId>JDA</artifactId>
|
||||||
<version>5.0.0-beta.9</version>
|
<version>5.2.1</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>club.minnced</groupId>
|
<groupId>club.minnced</groupId>
|
||||||
@ -96,7 +96,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.guava</groupId>
|
<groupId>com.google.guava</groupId>
|
||||||
<artifactId>guava</artifactId>
|
<artifactId>guava</artifactId>
|
||||||
<version>32.0.0-jre</version>
|
<version>32.1.3-jre</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
6
renovate.json
Normal file
6
renovate.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"local>Rainnny/renovate-config"
|
||||||
|
]
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license;
|
package me.braydon.license;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
|
package me.braydon.license.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public final class CryptographyUtils {
|
||||||
|
private static final String ALGORITHM = "RSA"; // The algorithm to use
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new key pair.
|
||||||
|
*
|
||||||
|
* @return the key pair
|
||||||
|
* @see KeyPair for key pair
|
||||||
|
*/
|
||||||
|
@NonNull @SneakyThrows
|
||||||
|
public static KeyPair generateKeyPair() {
|
||||||
|
KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM); // Create a generator
|
||||||
|
generator.initialize(2048); // Set the key size
|
||||||
|
return generator.generateKeyPair(); // Return our generated key pair
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the public key from the given file.
|
||||||
|
*
|
||||||
|
* @param keyFile the key file to read
|
||||||
|
* @return the public key
|
||||||
|
* @see PrivateKey for public key
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static PublicKey readPublicKey(@NonNull File keyFile) {
|
||||||
|
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Files.readAllBytes(keyFile.toPath())); // Get the key spec
|
||||||
|
return KeyFactory.getInstance(ALGORITHM).generatePublic(keySpec); // Return the public key from the key spec
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the private key from the given file.
|
||||||
|
*
|
||||||
|
* @param keyFile the key file to read
|
||||||
|
* @return the private key
|
||||||
|
* @see PrivateKey for private key
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static PrivateKey readPrivateKey(@NonNull File keyFile) {
|
||||||
|
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Files.readAllBytes(keyFile.toPath())); // Get the key spec
|
||||||
|
return KeyFactory.getInstance(ALGORITHM).generatePrivate(keySpec); // Return the private key from the key spec
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the given input with
|
||||||
|
* the provided private key.
|
||||||
|
*
|
||||||
|
* @param input the encrypted input
|
||||||
|
* @param privateKey the private key
|
||||||
|
* @return the decrypted result
|
||||||
|
* @see PrivateKey for private key
|
||||||
|
*/
|
||||||
|
@SneakyThrows @NonNull
|
||||||
|
public static String decryptMessage(@NonNull String input, @NonNull PrivateKey privateKey) {
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM); // Create the cipher
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, privateKey); // Set our mode and private key
|
||||||
|
return new String(cipher.doFinal(Base64.getDecoder().decode(input))); // Return our decrypted result
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.common;
|
package me.braydon.license.common;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.common;
|
package me.braydon.license.common;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.common;
|
package me.braydon.license.common;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.common;
|
package me.braydon.license.common;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
|
package me.braydon.license.controller;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import me.braydon.license.model.License;
|
||||||
|
import me.braydon.license.service.CryptographyService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/crypto", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public final class CryptographyController {
|
||||||
|
/**
|
||||||
|
* The {@link CryptographyService} to use.
|
||||||
|
*/
|
||||||
|
@NonNull private final CryptographyService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CryptographyController(@NonNull CryptographyService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the public key file.
|
||||||
|
*
|
||||||
|
* @return the response entity
|
||||||
|
* @see PublicKey for public key
|
||||||
|
* @see License for license
|
||||||
|
* @see ResponseEntity for response entity
|
||||||
|
*/
|
||||||
|
@GetMapping("/pub")
|
||||||
|
@ResponseBody
|
||||||
|
public ResponseEntity<Resource> publicKey() {
|
||||||
|
byte[] publicKey = service.getKeyPair().getPublic().getEncoded(); // Get the public key
|
||||||
|
String fileName = "public.key"; // The name of the file to download
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
|
||||||
|
.contentLength(publicKey.length)
|
||||||
|
.body(new ByteArrayResource(publicKey));
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.controller;
|
package me.braydon.license.controller;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import me.braydon.license.LicenseServer;
|
import me.braydon.license.common.CryptographyUtils;
|
||||||
import me.braydon.license.common.IPUtils;
|
import me.braydon.license.common.IPUtils;
|
||||||
|
import me.braydon.license.dto.LicenseCheckBodyDTO;
|
||||||
import me.braydon.license.dto.LicenseDTO;
|
import me.braydon.license.dto.LicenseDTO;
|
||||||
import me.braydon.license.exception.APIException;
|
import me.braydon.license.exception.APIException;
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
|
import me.braydon.license.service.CryptographyService;
|
||||||
import me.braydon.license.service.LicenseService;
|
import me.braydon.license.service.LicenseService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@ -16,6 +21,7 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,14 +30,20 @@ import java.util.Map;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
|
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public final class LicenseController {
|
public final class LicenseController {
|
||||||
|
/**
|
||||||
|
* The {@link CryptographyService} to use.
|
||||||
|
*/
|
||||||
|
@NonNull private final CryptographyService cryptographyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link LicenseService} to use.
|
* The {@link LicenseService} to use.
|
||||||
*/
|
*/
|
||||||
@NonNull private final LicenseService service;
|
@NonNull private final LicenseService licenseService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public LicenseController(@NonNull LicenseService service) {
|
public LicenseController(@NonNull CryptographyService cryptographyService, @NonNull LicenseService licenseService) {
|
||||||
this.service = service;
|
this.cryptographyService = cryptographyService;
|
||||||
|
this.licenseService = licenseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,53 +52,57 @@ public final class LicenseController {
|
|||||||
* @param body the body of the request
|
* @param body the body of the request
|
||||||
* @return the response entity
|
* @return the response entity
|
||||||
* @see License for license
|
* @see License for license
|
||||||
|
* @see LicenseCheckBodyDTO for body
|
||||||
* @see ResponseEntity for response entity
|
* @see ResponseEntity for response entity
|
||||||
*/
|
*/
|
||||||
@PostMapping("/check")
|
@PostMapping("/check")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public ResponseEntity<?> check(@NonNull HttpServletRequest request, @RequestBody @NonNull String body) {
|
public ResponseEntity<?> check(@NonNull HttpServletRequest request, @RequestBody @NonNull LicenseCheckBodyDTO body) {
|
||||||
try { // Attempt to check the license
|
try { // Attempt to check the license
|
||||||
String ip = IPUtils.getRealIp(request); // The IP of the requester
|
if (!body.isValid()) {
|
||||||
|
|
||||||
JsonObject jsonObject = LicenseServer.GSON.fromJson(body, JsonObject.class);
|
|
||||||
JsonElement key = jsonObject.get("key"); // Get the key
|
|
||||||
JsonElement product = jsonObject.get("product"); // Get the product
|
|
||||||
JsonElement hwid = jsonObject.get("hwid"); // Get the hwid
|
|
||||||
|
|
||||||
// Ensure the body keys aren't null
|
|
||||||
if (key.isJsonNull() || product.isJsonNull() || hwid.isJsonNull()) {
|
|
||||||
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid request body");
|
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid request body");
|
||||||
}
|
}
|
||||||
// Ensure the IP is valid
|
// Ensure the IP is valid
|
||||||
|
String ip = IPUtils.getRealIp(request); // The IP of the requester
|
||||||
if (IPUtils.getIpType(ip) == -1) {
|
if (IPUtils.getIpType(ip) == -1) {
|
||||||
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid IP address");
|
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid IP address");
|
||||||
}
|
}
|
||||||
// Ensure the HWID is valid
|
String key;
|
||||||
// TODO: improve :)
|
String hwid;
|
||||||
String hwidString = hwid.getAsString();
|
try {
|
||||||
|
PrivateKey privateKey = cryptographyService.getKeyPair().getPrivate(); // Get our private key
|
||||||
|
key = CryptographyUtils.decryptMessage(body.getKey(), privateKey); // Decrypt our license key
|
||||||
|
hwid = CryptographyUtils.decryptMessage(body.getHwid(), privateKey); // Decrypt our hwid
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
throw new APIException(HttpStatus.BAD_REQUEST, "Signature Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validating that the UUID is in the correct format
|
||||||
boolean invalidHwid = true;
|
boolean invalidHwid = true;
|
||||||
if (hwidString.contains("-")) {
|
if (hwid.contains("-")) {
|
||||||
int segments = hwidString.substring(0, hwidString.lastIndexOf("-")).split("-").length;
|
int segments = hwid.substring(0, hwid.lastIndexOf("-")).split("-").length;
|
||||||
if (segments == 4) {
|
if (segments == 4) {
|
||||||
invalidHwid = false;
|
invalidHwid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (invalidHwid) {
|
if (invalidHwid) { // Invalid HWID
|
||||||
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid HWID");
|
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid HWID");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the license
|
// Check the license
|
||||||
License license = service.check(
|
License license = licenseService.check(
|
||||||
key.getAsString(),
|
key,
|
||||||
product.getAsString(),
|
body.getProduct(),
|
||||||
ip,
|
ip,
|
||||||
hwidString
|
hwid
|
||||||
);
|
);
|
||||||
// Return OK with the license DTO
|
// Return OK with the license DTO
|
||||||
return ResponseEntity.ok(new LicenseDTO(
|
return ResponseEntity.ok(new LicenseDTO(
|
||||||
license.getDescription(),
|
license.getDescription(),
|
||||||
license.getOwnerSnowflake(),
|
license.getOwnerSnowflake(),
|
||||||
license.getOwnerName(),
|
license.getOwnerName(),
|
||||||
|
license.getPlan(),
|
||||||
|
license.getLatestVersion(),
|
||||||
license.getExpires()
|
license.getExpires()
|
||||||
));
|
));
|
||||||
} catch (APIException ex) { // Handle the exception
|
} catch (APIException ex) { // Handle the exception
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
|
package me.braydon.license.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import me.braydon.license.model.License;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data transfer object that contains
|
||||||
|
* the body for checking a {@link License}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public class LicenseCheckBodyDTO {
|
||||||
|
/**
|
||||||
|
* The license key to check.
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The product of the license to check.
|
||||||
|
*/
|
||||||
|
private String product;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hardware id of the user checking the license.
|
||||||
|
*/
|
||||||
|
private String hwid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are these params valid?
|
||||||
|
*
|
||||||
|
* @return whether the params are valid
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return key != null && product != null && hwid != null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.dto;
|
package me.braydon.license.dto;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
|
|
||||||
@ -35,6 +41,16 @@ public class LicenseDTO {
|
|||||||
*/
|
*/
|
||||||
private String ownerName;
|
private String ownerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plan for this license.
|
||||||
|
*/
|
||||||
|
@NonNull private String plan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest version of the product this license is for.
|
||||||
|
*/
|
||||||
|
@NonNull private String latestVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The optional expiration {@link Date} of this license.
|
* The optional expiration {@link Date} of this license.
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.exception;
|
package me.braydon.license.exception;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.exception;
|
package me.braydon.license.exception;
|
||||||
|
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.exception;
|
package me.braydon.license.exception;
|
||||||
|
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.exception;
|
package me.braydon.license.exception;
|
||||||
|
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.exception;
|
package me.braydon.license.exception;
|
||||||
|
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.model;
|
package me.braydon.license.model;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -9,6 +14,7 @@ import me.braydon.license.exception.LicenseHwidLimitExceededException;
|
|||||||
import me.braydon.license.exception.LicenseIpLimitExceededException;
|
import me.braydon.license.exception.LicenseIpLimitExceededException;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Field;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -44,6 +50,7 @@ public class License {
|
|||||||
* If this is -1, the license is not owned by anyone.
|
* If this is -1, the license is not owned by anyone.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
@Field("owner.snowflake")
|
||||||
private long ownerSnowflake;
|
private long ownerSnowflake;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,8 +59,19 @@ public class License {
|
|||||||
* If this is null, the license is not owned by anyone.
|
* If this is null, the license is not owned by anyone.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
@Field("owner.name")
|
||||||
private String ownerName;
|
private String ownerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plan for this license.
|
||||||
|
*/
|
||||||
|
@NonNull private String plan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latest version of the product this license is for.
|
||||||
|
*/
|
||||||
|
@NonNull private String latestVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of uses this license has.
|
* The amount of uses this license has.
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.repository;
|
package me.braydon.license.repository;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
|
package me.braydon.license.service;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import me.braydon.license.common.CryptographyUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j(topic = "Cryptography")
|
||||||
|
@Getter
|
||||||
|
public final class CryptographyService {
|
||||||
|
/**
|
||||||
|
* Our {@link KeyPair}.
|
||||||
|
*/
|
||||||
|
@NonNull private final KeyPair keyPair;
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public CryptographyService() {
|
||||||
|
File publicKeyFile = new File("public.key"); // The private key
|
||||||
|
File privateKeyFile = new File("private.key"); // The private key
|
||||||
|
if (!publicKeyFile.exists() || !privateKeyFile.exists()) { // Missing private key, generate new key pair.
|
||||||
|
keyPair = CryptographyUtils.generateKeyPair(); // Generate new key pair
|
||||||
|
writeKey(keyPair.getPublic().getEncoded(), publicKeyFile); // Write our public key
|
||||||
|
writeKey(keyPair.getPrivate().getEncoded(), privateKeyFile); // Write our private key
|
||||||
|
log.info("New key pair has been generated");
|
||||||
|
log.info(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load our private key from the file
|
||||||
|
keyPair = new KeyPair(CryptographyUtils.readPublicKey(publicKeyFile), CryptographyUtils.readPrivateKey(privateKeyFile));
|
||||||
|
log.info("Loaded private key from file " + privateKeyFile.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the given contents to the provided file.
|
||||||
|
*
|
||||||
|
* @param contents the content bytes to write
|
||||||
|
* @param file the file to write to
|
||||||
|
*/
|
||||||
|
private void writeKey(byte[] contents, @NonNull File file) {
|
||||||
|
try (FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||||
|
outputStream.write(contents);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.service;
|
package me.braydon.license.service;
|
||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
@ -33,7 +38,6 @@ import net.dv8tion.jda.api.requests.GatewayIntent;
|
|||||||
import org.mindrot.jbcrypt.BCrypt;
|
import org.mindrot.jbcrypt.BCrypt;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.info.BuildProperties;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@ -59,7 +63,7 @@ public final class DiscordService {
|
|||||||
/**
|
/**
|
||||||
* The version of this Springboot application.
|
* The version of this Springboot application.
|
||||||
*/
|
*/
|
||||||
@NonNull private final String applicationVersion;
|
@NonNull private final String applicationVersion = "n/a";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The salt to use for hashing license keys.
|
* The salt to use for hashing license keys.
|
||||||
@ -140,9 +144,9 @@ public final class DiscordService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DiscordService(@NonNull LicenseRepository licenseRepository, @NonNull BuildProperties buildProperties) {
|
public DiscordService(@NonNull LicenseRepository licenseRepository/*, @NonNull BuildProperties buildProperties*/) {
|
||||||
this.licenseRepository = licenseRepository;
|
this.licenseRepository = licenseRepository;
|
||||||
this.applicationVersion = buildProperties.getVersion();
|
// this.applicationVersion = buildProperties.getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct @SneakyThrows
|
@PostConstruct @SneakyThrows
|
||||||
@ -153,28 +157,34 @@ public final class DiscordService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Initialize the bot
|
// Initialize the bot
|
||||||
long before = System.currentTimeMillis();
|
new Thread(() -> {
|
||||||
log.info("Logging in..."); // Log that we're logging in
|
long before = System.currentTimeMillis();
|
||||||
jda = JDABuilder.createDefault(token)
|
log.info("Logging in..."); // Log that we're logging in
|
||||||
.enableIntents(
|
jda = JDABuilder.createDefault(token)
|
||||||
GatewayIntent.GUILD_MEMBERS
|
.enableIntents(
|
||||||
).setStatus(OnlineStatus.DO_NOT_DISTURB)
|
GatewayIntent.GUILD_MEMBERS
|
||||||
.setActivity(Activity.watching("your licenses"))
|
).setStatus(OnlineStatus.DO_NOT_DISTURB)
|
||||||
.addEventListeners(new EventHandler())
|
.setActivity(Activity.watching("your licenses"))
|
||||||
.build();
|
.addEventListeners(new EventHandler())
|
||||||
jda.awaitReady(); // Await JDA to be ready
|
.build();
|
||||||
|
try {
|
||||||
// Log that we're logged in
|
jda.awaitReady(); // Await JDA to be ready
|
||||||
log.info("Logged into {} in {}ms",
|
|
||||||
jda.getSelfUser().getAsTag(), System.currentTimeMillis() - before
|
// Log that we're logged in
|
||||||
);
|
log.info("Logged into {} in {}ms",
|
||||||
|
jda.getSelfUser().getEffectiveName(), System.currentTimeMillis() - before
|
||||||
// Registering slash commands
|
);
|
||||||
jda.updateCommands().addCommands(
|
|
||||||
Commands.slash("license", "Manage one of your licenses")
|
// Registering slash commands
|
||||||
.addOption(OptionType.STRING, "key", "The license key", true)
|
jda.updateCommands().addCommands(
|
||||||
.addOption(OptionType.STRING, "product", "The product the license is for", true)
|
Commands.slash("license", "Manage one of your licenses")
|
||||||
).queue();
|
.addOption(OptionType.STRING, "key", "The license key", true)
|
||||||
|
.addOption(OptionType.STRING, "product", "The product the license is for", true)
|
||||||
|
).queue();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
}, "Discord Bot Thread").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,6 +279,10 @@ public final class DiscordService {
|
|||||||
public class EventHandler extends ListenerAdapter {
|
public class EventHandler extends ListenerAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) {
|
public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) {
|
||||||
|
// Bot isn't ready, don't handle events
|
||||||
|
if (!isReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
User user = event.getUser(); // The command executor
|
User user = event.getUser(); // The command executor
|
||||||
|
|
||||||
// Handle the license command
|
// Handle the license command
|
||||||
@ -292,7 +306,7 @@ public final class DiscordService {
|
|||||||
License license = optionalLicense.get(); // The found license
|
License license = optionalLicense.get(); // The found license
|
||||||
String obfuscateKey = MiscUtils.obfuscateKey(key); // Obfuscate the key
|
String obfuscateKey = MiscUtils.obfuscateKey(key); // Obfuscate the key
|
||||||
long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L;
|
long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L;
|
||||||
long lastUsed = license.getLastUsed() == null ? -1L : license.getExpires().getTime() / 1000L;
|
long lastUsed = license.getLastUsed() == null ? -1L : license.getLastUsed().getTime() / 1000L;
|
||||||
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
|
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
|
||||||
.setColor(Color.BLUE)
|
.setColor(Color.BLUE)
|
||||||
.setTitle("Your License")
|
.setTitle("Your License")
|
||||||
@ -303,6 +317,7 @@ public final class DiscordService {
|
|||||||
expires == -1L ? "Never" : "<t:" + expires + ":R>",
|
expires == -1L ? "Never" : "<t:" + expires + ":R>",
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
.addField("Plan", license.getPlan(), true)
|
||||||
.addField("Uses", String.valueOf(license.getUses()), true)
|
.addField("Uses", String.valueOf(license.getUses()), true)
|
||||||
.addField("Last Used",
|
.addField("Last Used",
|
||||||
lastUsed == -1L ? "Never" : "<t:" + lastUsed + ":R>",
|
lastUsed == -1L ? "Never" : "<t:" + lastUsed + ":R>",
|
||||||
@ -339,6 +354,10 @@ public final class DiscordService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onButtonInteraction(@NonNull ButtonInteractionEvent event) {
|
public void onButtonInteraction(@NonNull ButtonInteractionEvent event) {
|
||||||
|
// Bot isn't ready, don't handle events
|
||||||
|
if (!isReady()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
User user = event.getUser(); // The user who clicked the button
|
User user = event.getUser(); // The user who clicked the button
|
||||||
String componentId = event.getComponentId(); // The button id
|
String componentId = event.getComponentId(); // The button id
|
||||||
|
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
|
||||||
|
*
|
||||||
|
* For inquiries, please contact braydonrainnny@gmail.com
|
||||||
|
*/
|
||||||
package me.braydon.license.service;
|
package me.braydon.license.service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import me.braydon.license.common.MiscUtils;
|
import me.braydon.license.common.MiscUtils;
|
||||||
|
import me.braydon.license.common.RandomUtils;
|
||||||
import me.braydon.license.exception.*;
|
import me.braydon.license.exception.*;
|
||||||
import me.braydon.license.model.License;
|
import me.braydon.license.model.License;
|
||||||
import me.braydon.license.repository.LicenseRepository;
|
import me.braydon.license.repository.LicenseRepository;
|
||||||
@ -53,6 +60,29 @@ public final class LicenseService {
|
|||||||
this.discordService = discordService;
|
this.discordService = discordService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a default license key
|
||||||
|
* when no other keys exist.
|
||||||
|
* TODO: Remove this in the future and replace with creation API route
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void onInitialize() {
|
||||||
|
if (repository.count() == 0L) { // No license keys found, create default
|
||||||
|
String licenseKey = RandomUtils.generateLicenseKey(); // The license key
|
||||||
|
create(
|
||||||
|
licenseKey,
|
||||||
|
"Example",
|
||||||
|
"Example",
|
||||||
|
0L,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
log.info("Generated default license: {}", licenseKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new license key.
|
* Create a new license key.
|
||||||
*
|
*
|
||||||
@ -76,6 +106,8 @@ public final class LicenseService {
|
|||||||
license.setDescription(description); // Use the given description, if any
|
license.setDescription(description); // Use the given description, if any
|
||||||
license.setOwnerSnowflake(ownerSnowflake);
|
license.setOwnerSnowflake(ownerSnowflake);
|
||||||
license.setOwnerName(ownerName);
|
license.setOwnerName(ownerName);
|
||||||
|
license.setPlan("Basic");
|
||||||
|
license.setLatestVersion("1.0");
|
||||||
license.setIps(new HashSet<>());
|
license.setIps(new HashSet<>());
|
||||||
license.setHwids(new HashSet<>());
|
license.setHwids(new HashSet<>());
|
||||||
license.setIpLimit(ipLimit); // Use the given IP limit
|
license.setIpLimit(ipLimit); // Use the given IP limit
|
||||||
|
Reference in New Issue
Block a user