diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml
index 88222f3..1778a89 100644
--- a/.forgejo/workflows/build.yml
+++ b/.forgejo/workflows/build.yml
@@ -29,5 +29,5 @@ jobs:
- name: ⬆️ Upload artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
- name: '${{ github.event.repository.name }}-${{ github.event.repository.default_branch }}-${{ github.ref_name }}.zip'
+ name: '${{ github.event.repository.name }}-${{ github.ref_name }}.zip'
path: build/libs/
\ No newline at end of file
diff --git a/README.md b/README.md
index 363df20..275b186 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,32 @@
+> [!NOTE]
+> Support for other mod loaders is not planned. PRs implementing such support will not be accepted, please fork this project instead.
+
+> [!WARNING]
+> This mod does not provide support for standard permission systems, and by default only verifies permissions by operator status (i.e. commands can only be run by operators).
+
+## Supported versions
+
+| Version | Support level |
+| ------- | ------------- |
+| 1.20.1 | ✅ Fully supported |
+| * | ❌ Not supported |
+
## Installing
-Download the latest release from the releases tab or go to the [latest release directly](https://code.lilyvex.dev/lily/oauth-fabric/releases/latest), then put it in your Fabric server's `mods` directory.
+Download the latest release from the releases tab or go to the [latest release directly](https://code.lilyvex.dev/lily/oauth-fabric/releases/latest), you may optionally choose to build from source, then put it in your Fabric server's `mods` directory.
+
+## Usage
+
+On initial load, this mod will create a commented configuration file. Edit the created file to contain the correct credentials for your OAuth provider, then restart the server.
+
+Players who are not registered will be kicked on join and given a link to the OAuth provider, where they can login to register for the server.
+
+Each new login with create a new session which will expire after a set period of time (usually defined by your OAuth provider).
+
+Use the `/oauth` command to see a list of all available commands.
## Building from source
@@ -30,4 +53,4 @@ gradlew.bat build
## Contributing
-Fork this repository and create a branch for your changes, then create a pull request for the `main` branch with a "why", "what", and "how" to explain your changes.
\ No newline at end of file
+Fork this repository and create a branch for your changes, then create a pull request for the `dev` branch with a "why", "what", and "how" to explain your changes.
diff --git a/build.gradle b/build.gradle
index 11870b0..b626148 100644
--- a/build.gradle
+++ b/build.gradle
@@ -30,6 +30,13 @@ loom {
}
+configurations {
+ include {
+ canBeResolved = true
+ canBeConsumed = true
+ }
+}
+
dependencies {
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
@@ -39,6 +46,12 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
+ // OAuth2 library
+ implementation 'com.google.oauth-client:google-oauth-client:1.39.0'
+
+ // Configuration library
+ include 'com.electronwill.night-config:toml:3.6.0'
+ implementation 'com.electronwill.night-config:toml:3.6.0'
}
processResources {
@@ -69,6 +82,10 @@ jar {
from("LICENSE") {
rename { "${it}_${inputs.properties.archivesName}"}
}
+
+ from {
+ configurations.include.collect { it.isDirectory() ? it : zipTree(it) }
+ }
}
// configure the maven publication
diff --git a/src/main/java/dev/lilyvex/oauthfabric/OAuthFabric.java b/src/main/java/dev/lilyvex/oauthfabric/OAuthFabric.java
index 06c0680..4eb8675 100644
--- a/src/main/java/dev/lilyvex/oauthfabric/OAuthFabric.java
+++ b/src/main/java/dev/lilyvex/oauthfabric/OAuthFabric.java
@@ -5,6 +5,8 @@ import net.fabricmc.api.ModInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import dev.lilyvex.oauthfabric.database.Database;
+
public class OAuthFabric implements ModInitializer {
public static final String MOD_ID = "oauth-fabric";
@@ -19,6 +21,12 @@ public class OAuthFabric implements ModInitializer {
// However, some things (like resources) may still be uninitialized.
// Proceed with mild caution.
- LOGGER.info("Hello Fabric world!");
+ OAuthFabricConfig oAuthFabricConfig = new OAuthFabricConfig();
+ oAuthFabricConfig.load();
+
+ Database database = new Database();
+ database.setDatabase(oAuthFabricConfig.getDatabase());
+
+ LOGGER.info("oauth-fabric: Initialized");
}
}
\ No newline at end of file
diff --git a/src/main/java/dev/lilyvex/oauthfabric/OAuthFabricConfig.java b/src/main/java/dev/lilyvex/oauthfabric/OAuthFabricConfig.java
new file mode 100644
index 0000000..809096a
--- /dev/null
+++ b/src/main/java/dev/lilyvex/oauthfabric/OAuthFabricConfig.java
@@ -0,0 +1,131 @@
+package dev.lilyvex.oauthfabric;
+
+import com.electronwill.nightconfig.core.CommentedConfig;
+import com.electronwill.nightconfig.core.io.ParsingException;
+import com.electronwill.nightconfig.core.io.ParsingMode;
+import com.electronwill.nightconfig.toml.TomlParser;
+import com.electronwill.nightconfig.toml.TomlWriter;
+
+import net.fabricmc.loader.api.FabricLoader;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OAuthFabricConfig {
+ private static final Logger LOGGER = LoggerFactory.getLogger("oauth-fabric");
+ private static final String DEFAULT_PROVIDER = "auth.lilyvex.dev";
+ private static final String DEFAULT_CLIENT_ID = "minecraft";
+ private static final String DEFAULT_CLIENT_SECRET = "";
+ private static final String DEFAULT_REDIRECT_URI = "mc.lilyvex.dev/oauth2";
+ private static final String DEFAULT_DATABASE = "sqlite";
+
+ private static final Path CONFIG_FILE_PATH = FabricLoader.getInstance().getConfigDir()
+ .resolve("oauth-fabric.toml")
+ .normalize();
+
+ private final CommentedConfig config = CommentedConfig.inMemory();
+
+ // URL to the OAuth provider (e.g. accounts.google.com, discord.com/oauth2)
+ private String provider;
+
+ // ID of the OAuth client. This is usually defined when creating the OAuth application.
+ private String client_id;
+
+ // Client secret
+ private String client_secret;
+
+ // URI to redirect to. Ensure that this URI is listed in the allowed redirect URIs section
+ // of your OAuth provider.
+ private String redirect_uri;
+
+ // Which database to use (SQLite is currently the only supported database).
+ private String database;
+
+ public String getProvider() {
+ return provider;
+ }
+
+ public String getClientId() {
+ return client_id;
+ }
+
+ public String getClientSecret() {
+ return client_secret;
+ }
+
+ public String getRedirectUri() {
+ return redirect_uri;
+ }
+
+ public String getDatabase() {
+ return database;
+ }
+
+ public void load() {
+ this.config.setComment("provider", "URL to the OAuth provider (e.g. accounts.google.com, discord.com/oauth2)");
+ this.config.set("provider", DEFAULT_PROVIDER);
+
+ this.config.setComment("client_id", "ID of the OAuth client. This is usually defined when creating the OAuth application.");
+ this.config.set("client_id", DEFAULT_CLIENT_ID);
+
+ this.config.setComment("client_secret", "");
+ this.config.set("client_secret", DEFAULT_CLIENT_SECRET);
+
+ this.config.setComment("redirect_uri", "URI to redirect to. Ensure that this URI is listed in the allowed redirect URIs section of your OAuth provider.");
+ this.config.set("redirect_uri", DEFAULT_REDIRECT_URI);
+
+ this.config.setComment("database", "Which database to use (SQLite is currently the only supported database).");
+ this.config.set("database", DEFAULT_DATABASE);
+
+ try {
+ this.loadFromFile(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ LOGGER.info("oauth-fabric: Configuration loaded.");
+ }
+
+ private void loadFromFile(boolean firstAttempt) throws IOException {
+ try (var reader = Files.newBufferedReader(CONFIG_FILE_PATH)) {
+ new TomlParser().parse(reader, this.config, ParsingMode.REPLACE);
+ } catch (NoSuchFileException | FileNotFoundException e) {
+ if (!firstAttempt) {
+ throw e;
+ }
+
+ this.copyDefaultFile();
+ this.loadFromFile(true);
+ } catch (ParsingException e) {
+ if (!firstAttempt) {
+ throw e;
+ }
+
+ var backupPath = CONFIG_FILE_PATH.resolveSibling("oauth-fabric.toml.old").toAbsolutePath().normalize();
+
+ LOGGER.error("oauth-fabric: Failed to parse configuration file, THIS IS BAD.", e);
+ LOGGER.error("oauth-fabric: Copying the corrupt file to \"{}\".", backupPath);
+ Files.copy(CONFIG_FILE_PATH, backupPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+ this.copyDefaultFile();
+ this.loadFromFile(false);
+ }
+ }
+
+ private void copyDefaultFile() throws IOException {
+ Files.createDirectories(CONFIG_FILE_PATH.getParent());
+
+ try (var writer = Files.newBufferedWriter(CONFIG_FILE_PATH, StandardCharsets.UTF_8)) {
+ new TomlWriter().write(this.config.unmodifiable(), writer);
+ } catch (NoSuchFileException | FileNotFoundException e) {
+ LOGGER.error("oauth-fabric: Failed to write default configuration file.");
+ }
+ }
+}
diff --git a/src/main/java/dev/lilyvex/oauthfabric/database/Database.java b/src/main/java/dev/lilyvex/oauthfabric/database/Database.java
new file mode 100644
index 0000000..6b348f8
--- /dev/null
+++ b/src/main/java/dev/lilyvex/oauthfabric/database/Database.java
@@ -0,0 +1,36 @@
+package dev.lilyvex.oauthfabric.database;
+
+public class Database implements IDatabase {
+ private IDatabase databaseClass;
+
+ public void setDatabase(String database) {
+ switch (database) {
+ case "sqlite":
+ databaseClass = new SQLite();
+ }
+ }
+
+ public void addUser(String uuid, String oauthId, String sessionToken) {
+ databaseClass.addUser(uuid, oauthId, sessionToken);
+ }
+
+ public void updateUser(String uuid, String oauthId, String sessionToken) {
+ databaseClass.updateUser(uuid, oauthId, sessionToken);
+ }
+
+ public void removeUser(String uuid) {
+ databaseClass.removeUser(uuid);
+ }
+
+ public boolean isRegisteredUser(String uuid) {
+ return databaseClass.isRegisteredUser(uuid);
+ }
+
+ public String getOauthId(String uuid) {
+ return databaseClass.getOauthId(uuid);
+ }
+
+ public String getSessionToken(String uuid) {
+ return databaseClass.getSessionToken(uuid);
+ }
+}
diff --git a/src/main/java/dev/lilyvex/oauthfabric/database/IDatabase.java b/src/main/java/dev/lilyvex/oauthfabric/database/IDatabase.java
new file mode 100644
index 0000000..f036f51
--- /dev/null
+++ b/src/main/java/dev/lilyvex/oauthfabric/database/IDatabase.java
@@ -0,0 +1,11 @@
+package dev.lilyvex.oauthfabric.database;
+
+public interface IDatabase {
+ public void addUser(String uuid, String oauthId, String sessionToken);
+ public void updateUser(String uuid, String oauthId, String sessionToken);
+ public void removeUser(String uuid);
+ public boolean isRegisteredUser(String uuid);
+
+ public String getOauthId(String uuid);
+ public String getSessionToken(String uuid);
+}
\ No newline at end of file
diff --git a/src/main/java/dev/lilyvex/oauthfabric/database/SQLite.java b/src/main/java/dev/lilyvex/oauthfabric/database/SQLite.java
new file mode 100644
index 0000000..340f1c0
--- /dev/null
+++ b/src/main/java/dev/lilyvex/oauthfabric/database/SQLite.java
@@ -0,0 +1,200 @@
+package dev.lilyvex.oauthfabric.database;
+
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.fabricmc.loader.api.FabricLoader;
+
+public class SQLite implements IDatabase {
+ private static final Logger LOGGER = LoggerFactory.getLogger("oauth-fabric");
+ private static final Path DATABASE_PATH = FabricLoader.getInstance().getConfigDir()
+ .resolve("oauth-fabric.db")
+ .normalize();
+
+ private static final String databaseUrl = "jdbc:sqlite:" + DATABASE_PATH;
+ private Connection connection = null;
+
+ private void createDatabase() throws SQLException {
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ Statement statement = connection.createStatement();
+ statement.executeUpdate("""
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY,
+ uuid TEXT UNIQUE,
+ oauth_id TEXT UNIQUE,
+ session_token TEXT UNIQUE
+ )
+ """);
+ } catch (SQLException e) {
+ LOGGER.error("oauth-fabric: Unable to create SQLite database");
+ } finally {
+ try {
+ if (connection != null) {
+ connection.close();
+ }
+ } catch (SQLException e) {
+ LOGGER.error("oauth-fabric: Could not close SQLite connection");
+ }
+ }
+ }
+
+ public void addUser(String uuid, String oauthId, String sessionToken) {
+ try {
+ createDatabase();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ PreparedStatement statement = connection.prepareStatement("""
+ INSERT INTO users (
+ uuid,
+ oauth_id,
+ session_token
+ ) VALUES (
+ ?,
+ ?,
+ ?
+ )
+ """);
+
+ statement.setString(1, uuid);
+ statement.setString(2, oauthId);
+ statement.setString(3, sessionToken);
+ statement.executeQuery();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void updateUser(String uuid, String oauthId, String sessionToken) {
+ try {
+ createDatabase();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ PreparedStatement statement = connection.prepareStatement("UPDATE users SET oauth_id = ?, session_token = ? WHERE uuid = ?");
+ statement.setString(1, oauthId);
+ statement.setString(2, sessionToken);
+ statement.setString(3, uuid);
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void removeUser(String uuid) {
+ try {
+ createDatabase();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ PreparedStatement statement = connection.prepareStatement("DELETE FROM users WHERE uuid = ?");
+ statement.setString(1, uuid);
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isRegisteredUser(String uuid) {
+ try {
+ createDatabase();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE uuid = ?");
+ statement.setString(1, uuid);
+ ResultSet resultSet = statement.executeQuery();
+
+ int results = 0;
+ while (resultSet.next()) {
+ results++;
+ }
+
+ if (results > 0) {
+ return true;
+ }
+
+ return false;
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getOauthId(String uuid) {
+ try {
+ createDatabase();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE uuid = ?");
+ statement.setString(1, uuid);
+ ResultSet resultSet = statement.executeQuery();
+
+ String userOauthId = "";
+
+ while (resultSet.next()) {
+ userOauthId = resultSet.getString("oauth_id");
+ }
+
+ return userOauthId;
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String getSessionToken(String uuid) {
+ try {
+ createDatabase();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ connection = DriverManager.getConnection(databaseUrl);
+
+ PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE uuid = ?");
+ statement.setString(1, uuid);
+ ResultSet resultSet = statement.executeQuery();
+
+ String userSessionToken = "";
+
+ while (resultSet.next()) {
+ userSessionToken = resultSet.getString("session_token");
+ }
+
+ return userSessionToken;
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index 4e243e3..894fb1b 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -3,15 +3,15 @@
"id": "oauth-fabric",
"version": "${version}",
"name": "OAuth Fabric",
- "description": "This is an example description! Tell everyone what your mod is about!",
+ "description": "OAuth2 authentication for Fabric Minecraft servers",
"authors": [
- "Me!"
+ "Lily Vex"
],
"contact": {
- "homepage": "https://fabricmc.net/",
- "sources": "https://github.com/FabricMC/fabric-example-mod"
+ "homepage": "https://code.lilyvex.dev/lily/oauth-fabric",
+ "sources": "https://code.lilyvex.dev/lily/oauth-fabric"
},
- "license": "CC0-1.0",
+ "license": "MIT",
"icon": "assets/oauth-fabric/icon.png",
"environment": "*",
"entrypoints": {