Compare commits

...
Sign in to create a new pull request.

11 commits
main ... dev

Author SHA1 Message Date
9283b9e925
fix: create database in main class
All checks were successful
build / build (push) Successful in 1m35s
2025-04-25 20:13:25 -04:00
e0f3d7e914
feat: sqlite database support and generic database class 2025-04-25 20:06:37 -04:00
f93f21d2c6
fix: use correct visibility for CONFIG_FILE_PATH 2025-04-25 19:22:47 -04:00
147c8fe3af
feat: add basic database interface 2025-04-25 19:20:33 -04:00
3d9cfa0207
chore: add usage sections and notes to README.md 2025-04-25 19:16:49 -04:00
fa187bb766
feat: configuration comments and getters
All checks were successful
build / build (push) Successful in 1m36s
2025-04-24 13:07:50 -04:00
cac64864c6
feat: add basic configuration management
All checks were successful
build / build (push) Successful in 1m31s
2025-04-23 22:47:02 -04:00
7482c3cdfe
feat: add night-config library 2025-04-23 22:25:54 -04:00
0af2923ef2
fix: missing mod metadata 2025-04-23 20:43:55 -04:00
af36e13307
feat: add Google OAuth library 2025-04-23 20:43:28 -04:00
ce267a4c66
fix(ci): removed default branch name from artifact name
All checks were successful
build / build (push) Successful in 1m17s
2025-04-23 20:18:19 -04:00
9 changed files with 435 additions and 9 deletions

View file

@ -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/

View file

@ -7,9 +7,32 @@
</div>
<hr>
> [!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.
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.

View file

@ -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

View file

@ -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");
}
}

View file

@ -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 = "<No default 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.");
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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": {