From 4b56d897a28831c530f9e8d86b90259d7a1eb19c Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Thu, 21 Aug 2025 15:06:11 -0600 Subject: [PATCH 01/10] chore: bump version to 0.0.5-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 50a7dc2..bd03fdd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ micronautVersion=4.8.3 -justserveCliVersion=0.0.4 \ No newline at end of file +justserveCliVersion=0.0.5-SNAPSHOT \ No newline at end of file From 94835a0dd51e5263c3fafe981c7dded3880fe64d Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 22 Aug 2025 15:41:28 -0600 Subject: [PATCH 02/10] refactor: organize files into usecase dirs --- .../{ => auth}/JustServeClientFilter.java | 2 +- .../{ => util}/JustServePrinter.java | 40 ++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) rename src/main/java/org/justserve/{ => auth}/JustServeClientFilter.java (98%) rename src/main/java/org/justserve/{ => util}/JustServePrinter.java (72%) diff --git a/src/main/java/org/justserve/JustServeClientFilter.java b/src/main/java/org/justserve/auth/JustServeClientFilter.java similarity index 98% rename from src/main/java/org/justserve/JustServeClientFilter.java rename to src/main/java/org/justserve/auth/JustServeClientFilter.java index 8825843..932da88 100644 --- a/src/main/java/org/justserve/JustServeClientFilter.java +++ b/src/main/java/org/justserve/auth/JustServeClientFilter.java @@ -1,4 +1,4 @@ -package org.justserve; +package org.justserve.auth; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; diff --git a/src/main/java/org/justserve/JustServePrinter.java b/src/main/java/org/justserve/util/JustServePrinter.java similarity index 72% rename from src/main/java/org/justserve/JustServePrinter.java rename to src/main/java/org/justserve/util/JustServePrinter.java index 87ea710..90afb0b 100644 --- a/src/main/java/org/justserve/JustServePrinter.java +++ b/src/main/java/org/justserve/util/JustServePrinter.java @@ -1,7 +1,8 @@ -package org.justserve; +package org.justserve.util; import org.fusesource.jansi.Ansi; +import static java.lang.System.lineSeparator; import static java.util.Arrays.stream; import static org.fusesource.jansi.Ansi.ansi; @@ -18,7 +19,7 @@ public final class JustServePrinter { private final static Ansi yellow = ansi().fgRgb(225, 188, 33); private final static Ansi normalStyle = ansi().reset(); - private final static Ansi titleStyle = blue; + private final static Ansi titleStyle = blue.bold(); private final static Ansi emphasisStyle = orange; private final static Ansi warningStyle = yellow; private final static Ansi errorTitleStyle = red.bold(); @@ -32,14 +33,23 @@ private JustServePrinter() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - private static void jsPrint(String message, Ansi... style) { + private static String applyStyle(String message, Ansi... style) { Ansi styledMessage = stream(style).reduce(ansi(), Ansi::a, Ansi::a); - System.out.println(styledMessage.a(message).reset()); + String[] lines = message.split(lineSeparator()); + StringBuilder sb = new StringBuilder(); + for (String line : lines) { + sb.append(styledMessage.a(line).reset()); + } + return sb.toString(); + } + + private static void jsPrint(String message, Ansi... style) { + System.out.println(applyStyle(message, style)); } + private static void jsPrintErr(String message, Ansi... style) { - Ansi styledMessage = stream(style).reduce(ansi(), Ansi::a, Ansi::a); - System.err.println(styledMessage.a(message).reset()); + System.err.println(applyStyle(message, style)); } /** @@ -51,6 +61,24 @@ public static void printNormal(String message) { jsPrint(message, normalStyle); } + /** + * Returns a String stylized in Orange. + * + * @param message The message to print. + */ + public static String styleTitle(String message) { + return applyStyle(message, titleStyle); + } + + /** + * Returns a String stylized in Blue. + * + * @param message The message to print. + */ + public static String styleOrange(String message) { + return applyStyle(message, orange); + } + /** * Prints an error message to the standard error stream in red. * From 1e02c2e51978246ac0ead6bc975e325317abf9e9 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 22 Aug 2025 15:42:16 -0600 Subject: [PATCH 03/10] feat: add version provider --- src/main/java/org/justserve/BaseCommand.java | 18 +++++-------- .../org/justserve/util/VersionProvider.java | 26 +++++++++++++++++++ .../org/justserve/BaseCommandSpec.groovy | 9 ++++++- 3 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/justserve/util/VersionProvider.java diff --git a/src/main/java/org/justserve/BaseCommand.java b/src/main/java/org/justserve/BaseCommand.java index 76b78ea..746f510 100644 --- a/src/main/java/org/justserve/BaseCommand.java +++ b/src/main/java/org/justserve/BaseCommand.java @@ -8,26 +8,24 @@ import jakarta.inject.Provider; import org.justserve.client.UserClient; import org.justserve.model.UserHashRequestByEmail; +import org.justserve.util.VersionProvider; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.jansi.graalvm.AnsiConsole; -import static org.justserve.JustServePrinter.printError; -import static org.justserve.JustServePrinter.printNormal; +import static org.justserve.util.JustServePrinter.printError; +import static org.justserve.util.JustServePrinter.printNormal; -@Command(name = "justserve", +@Command(name = "justserve", versionProvider = VersionProvider.class, description = "justserve-cli is a terminal tool to help specialists and admin using JustServe") public class BaseCommand implements Runnable { @Option(names = {"-e", "--email"}, description = "email for the user whose temporary password will be generated") String email; - @Option(names = {"version", "--version", "-v"}) + @Option(names = {"version", "--version", "-v"}, versionHelp = true, description = "print version info and exit") boolean version = false; - @Value("${micronaut.application.version}") - String justserveCliVersion; - @Inject Provider userClientProvider; @@ -42,12 +40,8 @@ public static void main(String[] args) { } public void run() { - if (version) { - printNormal(justserveCliVersion); - return; - } HttpResponse response; - if ("i-need-to-be-defined".equals(token) || null == token ) { + if ("i-need-to-be-defined".equals(token) || null == token) { printError(("NO AUTHENTICATION PROVIDED" + System.lineSeparator() + "The Authentication token is not assigned as an environment variable." + System.lineSeparator() + "Please define the environment variable \"JUSTSERVE_TOKEN\" and try again.")); diff --git a/src/main/java/org/justserve/util/VersionProvider.java b/src/main/java/org/justserve/util/VersionProvider.java new file mode 100644 index 0000000..9e7c8ec --- /dev/null +++ b/src/main/java/org/justserve/util/VersionProvider.java @@ -0,0 +1,26 @@ +package org.justserve.util; + +import io.micronaut.context.annotation.Value; +import picocli.CommandLine; + +import static org.justserve.util.JustServePrinter.styleOrange; +import static org.justserve.util.JustServePrinter.styleTitle; + +public class VersionProvider implements CommandLine.IVersionProvider { + + @Value("${micronaut.application.version}") + String justserveCliVersion; + + String fancyPrintout = """ + _ _ ____ + | | | | / ___| + | |_ _ ___| |_| (___ ___ _ ____ _____ + | | | | / __| __|\\___ \\ / _ \\ '__\\ \\ / / _ \\ + ___| | |_| \\__ \\ |_ ____) | __/ | \\ V / __/ + |____/ \\__,_|___/\\__|_____/ \\___|_| \\_/ \\___|"""; + + @Override + public String[] getVersion() { + return new String[]{styleOrange(fancyPrintout), styleTitle(justserveCliVersion)}; + } +} diff --git a/src/test/groovy/org/justserve/BaseCommandSpec.groovy b/src/test/groovy/org/justserve/BaseCommandSpec.groovy index 0786f85..7d54604 100644 --- a/src/test/groovy/org/justserve/BaseCommandSpec.groovy +++ b/src/test/groovy/org/justserve/BaseCommandSpec.groovy @@ -18,7 +18,14 @@ class BaseCommandSpec extends JustServeSpec { props.load(stream) } def ansi = "\\u001B\\[[;\\d]*m" - cliVersion = Pattern.compile("^${ansi}" + props.getProperty('justserveCliVersion') + "${ansi}\\s*\$") + def staticText = """ _ _ ____ + | | | | / ___| + | |_ _ ___| |_| (___ ___ _ ____ _____ + | | | | / __| __|\\___ \\ / _ \\ '__\\ \\ / / _ \\ + ___| | |_| \\__ \\ |_ ____) | __/ | \\ V / __/ +|____/ \\__,_|___/\\__|_____/ \\___|_| \\_/ \\___|"""; + cliVersion = Pattern.compile("^${ansi}${Pattern.quote(staticText)}${ansi}\\s*${ansi}" + + Pattern.quote(props.getProperty('justserveCliVersion')) + "${ansi}\\s*\$") blankRegex = Pattern.compile "^\\s*\$" successRegex = Pattern.compile("^${ansi}\\w+${ansi}\\s*\$") errorRegex = Pattern.compile("(?is)^${ansi}received an unexpected response from JustServe:${ansi}.*\\d+ \\(.*?\\)${ansi}\\s*\$") From 03d494a1ea6351c1a1fd92e7c20aa022552a4c82 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 22 Aug 2025 16:12:51 -0600 Subject: [PATCH 04/10] refactor: change orange RGB value --- src/main/java/org/justserve/util/JustServePrinter.java | 10 +++++----- src/main/java/org/justserve/util/VersionProvider.java | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/justserve/util/JustServePrinter.java b/src/main/java/org/justserve/util/JustServePrinter.java index 90afb0b..467de7b 100644 --- a/src/main/java/org/justserve/util/JustServePrinter.java +++ b/src/main/java/org/justserve/util/JustServePrinter.java @@ -14,13 +14,13 @@ public final class JustServePrinter { private final static Ansi blue = ansi().fgRgb(0, 158, 185); - private final static Ansi orange = ansi().fgRgb(239, 94, 57); + private final static Ansi orange = ansi().fgRgb(255, 140, 0); // OG color is 239, 94, 57 private final static Ansi red = ansi().fgRgb(233, 59, 84); // I definitely eyeballed this one private final static Ansi yellow = ansi().fgRgb(225, 188, 33); private final static Ansi normalStyle = ansi().reset(); private final static Ansi titleStyle = blue.bold(); - private final static Ansi emphasisStyle = orange; + private final static Ansi emphasisStyle = orange.bold(); private final static Ansi warningStyle = yellow; private final static Ansi errorTitleStyle = red.bold(); private final static Ansi errorInfoStyle = ansi().reset(); @@ -71,12 +71,12 @@ public static String styleTitle(String message) { } /** - * Returns a String stylized in Blue. + * Returns a String stylized in the designated emphasis style. * * @param message The message to print. */ - public static String styleOrange(String message) { - return applyStyle(message, orange); + public static String styleEmphasis(String message) { + return applyStyle(message, emphasisStyle); } /** diff --git a/src/main/java/org/justserve/util/VersionProvider.java b/src/main/java/org/justserve/util/VersionProvider.java index 9e7c8ec..082db41 100644 --- a/src/main/java/org/justserve/util/VersionProvider.java +++ b/src/main/java/org/justserve/util/VersionProvider.java @@ -3,7 +3,7 @@ import io.micronaut.context.annotation.Value; import picocli.CommandLine; -import static org.justserve.util.JustServePrinter.styleOrange; +import static org.justserve.util.JustServePrinter.styleEmphasis; import static org.justserve.util.JustServePrinter.styleTitle; public class VersionProvider implements CommandLine.IVersionProvider { @@ -21,6 +21,6 @@ public class VersionProvider implements CommandLine.IVersionProvider { @Override public String[] getVersion() { - return new String[]{styleOrange(fancyPrintout), styleTitle(justserveCliVersion)}; + return new String[]{styleEmphasis(fancyPrintout), styleTitle(justserveCliVersion)}; } } From b4e88da89affef3e1b7d363a223b54cb42b5d8bb Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger Date: Sat, 23 Aug 2025 15:30:45 -0600 Subject: [PATCH 05/10] fix: read file endings correctly --- src/main/java/org/justserve/util/JustServePrinter.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/org/justserve/util/JustServePrinter.java b/src/main/java/org/justserve/util/JustServePrinter.java index 467de7b..6a01c62 100644 --- a/src/main/java/org/justserve/util/JustServePrinter.java +++ b/src/main/java/org/justserve/util/JustServePrinter.java @@ -2,7 +2,6 @@ import org.fusesource.jansi.Ansi; -import static java.lang.System.lineSeparator; import static java.util.Arrays.stream; import static org.fusesource.jansi.Ansi.ansi; @@ -35,12 +34,7 @@ private JustServePrinter() { private static String applyStyle(String message, Ansi... style) { Ansi styledMessage = stream(style).reduce(ansi(), Ansi::a, Ansi::a); - String[] lines = message.split(lineSeparator()); - StringBuilder sb = new StringBuilder(); - for (String line : lines) { - sb.append(styledMessage.a(line).reset()); - } - return sb.toString(); + return styledMessage.a(message).reset().toString(); } private static void jsPrint(String message, Ansi... style) { From 9b56be13992b2d7ef97d0890cb1e2ed02e1629ae Mon Sep 17 00:00:00 2001 From: Jonathan Zollinger <62955101+Jonathan-Zollinger@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:16:42 -0600 Subject: [PATCH 06/10] refactor/parse into subcommands (#50) --- build.gradle.kts | 4 +- compose.yml | 26 +++++++ gradle.properties | 3 +- .../org/justserve/cli/JustServeCommand.java | 28 ++++++++ .../justserve/cli/command/BaseCommand.java | 70 +++++++++++++++++++ .../cli/command/CommonOptionsMixin.java | 34 +++++++++ .../justserve/cli/command/ConsoleOutput.java | 43 ++++++++++++ .../command/GetTempPassword.java} | 28 +++----- .../{ => cli}/util/JustServePrinter.java | 2 +- .../util/JustServeVersionProvider.java} | 8 +-- .../justserve/client/UserClientSpec.groovy | 2 +- .../{ => command}/BaseCommandSpec.groovy | 41 ++--------- .../command/GetTempPasswordSpec.groovy | 33 +++++++++ 13 files changed, 261 insertions(+), 61 deletions(-) create mode 100644 compose.yml create mode 100644 src/main/java/org/justserve/cli/JustServeCommand.java create mode 100644 src/main/java/org/justserve/cli/command/BaseCommand.java create mode 100644 src/main/java/org/justserve/cli/command/CommonOptionsMixin.java create mode 100644 src/main/java/org/justserve/cli/command/ConsoleOutput.java rename src/main/java/org/justserve/{BaseCommand.java => cli/command/GetTempPassword.java} (67%) rename src/main/java/org/justserve/{ => cli}/util/JustServePrinter.java (99%) rename src/main/java/org/justserve/{util/VersionProvider.java => cli/util/JustServeVersionProvider.java} (73%) rename src/test/groovy/org/justserve/{ => command}/BaseCommandSpec.groovy (64%) create mode 100644 src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy diff --git a/build.gradle.kts b/build.gradle.kts index 61aa646..2b5f4b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,8 @@ dependencies { implementation("info.picocli:picocli") implementation("info.picocli:picocli-jansi-graalvm:1.2.0") implementation("org.fusesource.jansi:jansi:2.4.2") + implementation("info.picocli:picocli-shell-jline3:4.7.6") + implementation("org.jline:jline:3.30.5") implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut.picocli:micronaut-picocli") implementation("io.micronaut.serde:micronaut-serde-jackson") @@ -36,7 +38,7 @@ dependencies { application { - mainClass = "org.justserve.BaseCommand" + mainClass = "org.justserve.cli.JustServeCommand" } java { diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..41073ae --- /dev/null +++ b/compose.yml @@ -0,0 +1,26 @@ +services: + linux-shell: + image: ghcr.io/graalvm/native-image-community:21-muslib + container_name: justserve-linux-shell + working_dir: /app + volumes: + - .:/app + env_file: .env + entrypoint: /bin/sh + command: + - -c + - "mkdir -p /temp_app && cp -r . /temp_app/ && cd /temp_app && microdnf install -y findutils && sed -i 's/\r$//' ./gradlew && if [ -t 0 ]; then /bin/bash; else tail -f /dev/null; fi" + stdin_open: true + tty: true + + linux-test: + image: ghcr.io/graalvm/native-image-community:21-muslib + container_name: justserve-linux-test + working_dir: /app + volumes: + - .:/app + env_file: .env + entrypoint: /bin/sh + command: + - -c + - "mkdir -p /temp_app && cp -r . /temp_app/ && cd /temp_app && microdnf install -y findutils && sed -i 's/\r$//' ./gradlew && ./gradlew test" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index bd03fdd..b1b5a5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ micronautVersion=4.8.3 -justserveCliVersion=0.0.5-SNAPSHOT \ No newline at end of file +justserveCliVersion=0.0.5-SNAPSHOT +org.gradle.console=rich \ No newline at end of file diff --git a/src/main/java/org/justserve/cli/JustServeCommand.java b/src/main/java/org/justserve/cli/JustServeCommand.java new file mode 100644 index 0000000..67a472f --- /dev/null +++ b/src/main/java/org/justserve/cli/JustServeCommand.java @@ -0,0 +1,28 @@ +package org.justserve.cli; + +import io.micronaut.configuration.picocli.PicocliRunner; +import org.justserve.cli.command.BaseCommand; +import org.justserve.cli.command.GetTempPassword; +import org.justserve.cli.util.JustServeVersionProvider; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParameterException; +import picocli.jansi.graalvm.AnsiConsole; + +@Command(subcommands = GetTempPassword.class, + mixinStandardHelpOptions = true, + name = "justserve", versionProvider = JustServeVersionProvider.class, + description = "justserve-cli is a terminal tool to help specialists and admin using JustServe") +public class JustServeCommand extends BaseCommand implements Runnable { + + public static void main(String[] args) { + try (AnsiConsole ignored = AnsiConsole.windowsInstall()) { + PicocliRunner.run(JustServeCommand.class, args); + } + + } + + @Override + public void run() { + throw new ParameterException(spec.commandLine(), "No command specified"); + } +} diff --git a/src/main/java/org/justserve/cli/command/BaseCommand.java b/src/main/java/org/justserve/cli/command/BaseCommand.java new file mode 100644 index 0000000..29da8f6 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/BaseCommand.java @@ -0,0 +1,70 @@ +package org.justserve.cli.command; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.ReflectiveAccess; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Spec; + +import java.io.PrintWriter; +import java.util.Optional; + +import static picocli.CommandLine.Help.Ansi.AUTO; + +@Command +public class BaseCommand implements ConsoleOutput{ + + @Spec + @ReflectiveAccess + protected CommandSpec spec; + + + @Override + public void out(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string(message))); + } + + public void err(String message) { + errWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,red | Error|@ " + message))); + } + + public void warning(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,red | Warning|@ " + message))); + } + + @Override + public void green(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,green " + message + "|@"))); + } + + @Override + public void red(String message) { + outWriter().ifPresent(writer -> writer.println(AUTO.string("@|bold,red " + message + "|@"))); + } + + @Override + public boolean showStacktrace() { + return false; + } + + @Override + public boolean verbose() { + return false; + } + + + @NonNull + public Optional outWriter() { + return getSpec().map(spec -> spec.commandLine().getOut()); + } + + @NonNull + public Optional errWriter() { + return getSpec().map(spec -> spec.commandLine().getErr()); + } + + @NonNull + public Optional getSpec() { + return Optional.ofNullable(spec); + } +} diff --git a/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java b/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java new file mode 100644 index 0000000..527e5c0 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java @@ -0,0 +1,34 @@ +package org.justserve.cli.command; + +import io.micronaut.core.annotation.ReflectiveAccess; +import org.justserve.cli.util.JustServeVersionProvider; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +/** + * Mixin that adds help, version and other common options to a command. Example usage: + *
+ * @Command(name = "command")
+ * class App {
+ *     @Mixin
+ *     CommonOptionsMixin commonOptions // adds common options to the command
+ *
+ *     // ...
+ * }
+ * 
+ * + * @author Remko Popma + * @version 1.0 + */ +@Command(mixinStandardHelpOptions = true, versionProvider = JustServeVersionProvider.class) +@SuppressWarnings("checkstyle:VisibilityModifier") +public class CommonOptionsMixin { + + @Option(names = {"-x", "--stacktrace"}, defaultValue = "false", description = "Show full stack trace when exceptions occur.") + @ReflectiveAccess + public boolean showStacktrace; + + @Option(names = {"-v", "--verbose"}, defaultValue = "false", description = "Create verbose output.") + @ReflectiveAccess + public boolean verbose; +} \ No newline at end of file diff --git a/src/main/java/org/justserve/cli/command/ConsoleOutput.java b/src/main/java/org/justserve/cli/command/ConsoleOutput.java new file mode 100644 index 0000000..00c03b0 --- /dev/null +++ b/src/main/java/org/justserve/cli/command/ConsoleOutput.java @@ -0,0 +1,43 @@ +package org.justserve.cli.command; + +public interface ConsoleOutput { + ConsoleOutput NOOP = new ConsoleOutput() { + @Override + public void out(String message) { } + + @Override + public void err(String message) { } + + @Override + public void warning(String message) { } + + @Override + public boolean showStacktrace() { + return false; + } + + @Override + public boolean verbose() { + return false; + } + + }; + + void out(String message); + + void err(String message); + + void warning(String message); + + boolean showStacktrace(); + + boolean verbose(); + + default void green(String message) { + out(message); + } + + default void red(String message) { + out(message); + } +} diff --git a/src/main/java/org/justserve/BaseCommand.java b/src/main/java/org/justserve/cli/command/GetTempPassword.java similarity index 67% rename from src/main/java/org/justserve/BaseCommand.java rename to src/main/java/org/justserve/cli/command/GetTempPassword.java index 746f510..399d28a 100644 --- a/src/main/java/org/justserve/BaseCommand.java +++ b/src/main/java/org/justserve/cli/command/GetTempPassword.java @@ -1,44 +1,34 @@ -package org.justserve; +package org.justserve.cli.command; -import io.micronaut.configuration.picocli.PicocliRunner; import io.micronaut.context.annotation.Value; +import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.http.HttpResponse; import io.micronaut.http.client.exceptions.HttpClientResponseException; import jakarta.inject.Inject; import jakarta.inject.Provider; import org.justserve.client.UserClient; import org.justserve.model.UserHashRequestByEmail; -import org.justserve.util.VersionProvider; import picocli.CommandLine.Command; import picocli.CommandLine.Option; -import picocli.jansi.graalvm.AnsiConsole; -import static org.justserve.util.JustServePrinter.printError; -import static org.justserve.util.JustServePrinter.printNormal; +import static org.justserve.cli.util.JustServePrinter.printError; +import static org.justserve.cli.util.JustServePrinter.printNormal; -@Command(name = "justserve", versionProvider = VersionProvider.class, - description = "justserve-cli is a terminal tool to help specialists and admin using JustServe") -public class BaseCommand implements Runnable { +@Command(name = "getTempPassword", description = "get a temporary password for a user") +public class GetTempPassword extends BaseCommand implements Runnable { + @ReflectiveAccess @Option(names = {"-e", "--email"}, description = "email for the user whose temporary password will be generated") String email; - @Option(names = {"version", "--version", "-v"}, versionHelp = true, description = "print version info and exit") - boolean version = false; - @Inject + @ReflectiveAccess Provider userClientProvider; @Value("${justserve.token}") String token; - public static void main(String[] args) { - try (AnsiConsole ignored = AnsiConsole.windowsInstall()) { - PicocliRunner.run(BaseCommand.class, args); - } - - } - + @Override public void run() { HttpResponse response; if ("i-need-to-be-defined".equals(token) || null == token) { diff --git a/src/main/java/org/justserve/util/JustServePrinter.java b/src/main/java/org/justserve/cli/util/JustServePrinter.java similarity index 99% rename from src/main/java/org/justserve/util/JustServePrinter.java rename to src/main/java/org/justserve/cli/util/JustServePrinter.java index 6a01c62..643431d 100644 --- a/src/main/java/org/justserve/util/JustServePrinter.java +++ b/src/main/java/org/justserve/cli/util/JustServePrinter.java @@ -1,4 +1,4 @@ -package org.justserve.util; +package org.justserve.cli.util; import org.fusesource.jansi.Ansi; diff --git a/src/main/java/org/justserve/util/VersionProvider.java b/src/main/java/org/justserve/cli/util/JustServeVersionProvider.java similarity index 73% rename from src/main/java/org/justserve/util/VersionProvider.java rename to src/main/java/org/justserve/cli/util/JustServeVersionProvider.java index 082db41..f67d90a 100644 --- a/src/main/java/org/justserve/util/VersionProvider.java +++ b/src/main/java/org/justserve/cli/util/JustServeVersionProvider.java @@ -1,12 +1,12 @@ -package org.justserve.util; +package org.justserve.cli.util; import io.micronaut.context.annotation.Value; import picocli.CommandLine; -import static org.justserve.util.JustServePrinter.styleEmphasis; -import static org.justserve.util.JustServePrinter.styleTitle; +import static org.justserve.cli.util.JustServePrinter.styleEmphasis; +import static org.justserve.cli.util.JustServePrinter.styleTitle; -public class VersionProvider implements CommandLine.IVersionProvider { +public class JustServeVersionProvider implements CommandLine.IVersionProvider { @Value("${micronaut.application.version}") String justserveCliVersion; diff --git a/src/test/groovy/org/justserve/client/UserClientSpec.groovy b/src/test/groovy/org/justserve/client/UserClientSpec.groovy index f868993..5f0ffbb 100644 --- a/src/test/groovy/org/justserve/client/UserClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/UserClientSpec.groovy @@ -19,7 +19,7 @@ class UserClientSpec extends JustServeSpec { userClient = ctx.getBean(UserClient) } - def "get tempPassword for #email and "() { + def "get tempPassword for #email"() { when: HttpResponse response = null def caughtException = null diff --git a/src/test/groovy/org/justserve/BaseCommandSpec.groovy b/src/test/groovy/org/justserve/command/BaseCommandSpec.groovy similarity index 64% rename from src/test/groovy/org/justserve/BaseCommandSpec.groovy rename to src/test/groovy/org/justserve/command/BaseCommandSpec.groovy index 7d54604..200a627 100644 --- a/src/test/groovy/org/justserve/BaseCommandSpec.groovy +++ b/src/test/groovy/org/justserve/command/BaseCommandSpec.groovy @@ -1,7 +1,9 @@ -package org.justserve +package org.justserve.command import io.micronaut.configuration.picocli.PicocliRunner import io.micronaut.context.ApplicationContext +import org.justserve.JustServeSpec +import org.justserve.cli.JustServeCommand import spock.lang.Shared import spock.lang.Unroll @@ -10,7 +12,7 @@ import java.util.regex.Pattern class BaseCommandSpec extends JustServeSpec { @Shared - Pattern cliVersion, ansiRegex, blankRegex, successRegex, errorRegex, tokenNotSetRegex + Pattern cliVersion, blankRegex, successRegex, errorRegex, tokenNotSetRegex def setupSpec() { def props = new Properties() @@ -23,7 +25,7 @@ class BaseCommandSpec extends JustServeSpec { | |_ _ ___| |_| (___ ___ _ ____ _____ | | | | / __| __|\\___ \\ / _ \\ '__\\ \\ / / _ \\ ___| | |_| \\__ \\ |_ ____) | __/ | \\ V / __/ -|____/ \\__,_|___/\\__|_____/ \\___|_| \\_/ \\___|"""; +|____/ \\__,_|___/\\__|_____/ \\___|_| \\_/ \\___|""" cliVersion = Pattern.compile("^${ansi}${Pattern.quote(staticText)}${ansi}\\s*${ansi}" + Pattern.quote(props.getProperty('justserveCliVersion')) + "${ansi}\\s*\$") blankRegex = Pattern.compile "^\\s*\$" @@ -34,31 +36,6 @@ class BaseCommandSpec extends JustServeSpec { "${ansi}Please define the environment variable \"JUSTSERVE_TOKEN\" and try again\\.${ansi}\\s*\$") } - @Unroll("command with args: calling 'justserve #flag #email' works as expected") - def "commands to query temporary password should behave as expected with or without authentication"() { - when: - def (outputStream, errorStream) = executeCommand(context as ApplicationContext, [flag, email] as String[]) - - then: - if (context == noAuthCtx) { - verifyAll { - outputStream.matches(blankRegex) - errorStream.matches(tokenNotSetRegex) - } - } else if (userEmail.equalsIgnoreCase(email as String)) { - verifyAll { - outputStream.matches(successRegex) - errorStream.matches(blankRegex) - } - } else { - verifyAll { - outputStream.matches(blankRegex) - errorStream.matches(errorRegex) - } - } - where: - [flag, email, context] << [['-e', '--email'], [userEmail, "notanemail@mail.moc"], [noAuthCtx, ctx]].combinations() - } @Unroll def "querying version returns current version, with or without authentication"() { @@ -72,7 +49,7 @@ class BaseCommandSpec extends JustServeSpec { } where: - [args, context] << [[['-v'], ['--version'], ['version']], [noAuthCtx, ctx]].combinations() + [args, context] << [[['-V'], ['--version']], [noAuthCtx, ctx]].combinations() } /** @@ -89,12 +66,8 @@ class BaseCommandSpec extends JustServeSpec { OutputStream err = new ByteArrayOutputStream() System.setOut(new PrintStream(out)) System.setErr(new PrintStream(err)) - PicocliRunner.run(BaseCommand.class, ctx, args) + PicocliRunner.run(JustServeCommand.class, ctx, args) return new String[]{out.toString(), err.toString()} } - String stripColor(String string) { - return ansiRegex.matcher(string).replaceAll("") - } - } diff --git a/src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy b/src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy new file mode 100644 index 0000000..305d8da --- /dev/null +++ b/src/test/groovy/org/justserve/command/GetTempPasswordSpec.groovy @@ -0,0 +1,33 @@ +package org.justserve.command + +import io.micronaut.context.ApplicationContext +import spock.lang.Unroll + +class GetTempPasswordSpec extends BaseCommandSpec { + + @Unroll("command with args: calling 'justserve #flag #email' works as expected") + def "commands to query temporary password should behave as expected with or without authentication"() { + when: + def (outputStream, errorStream) = executeCommand(context as ApplicationContext, ["getTempPassword", flag, email] as String[]) + + then: + if (context == noAuthCtx) { + verifyAll { + outputStream.matches(blankRegex) + errorStream.matches(tokenNotSetRegex) + } + } else if (userEmail.equalsIgnoreCase(email as String)) { + verifyAll { + outputStream.matches(successRegex) + errorStream.matches(blankRegex) + } + } else { + verifyAll { + outputStream.matches(blankRegex) + errorStream.matches(errorRegex) + } + } + where: + [flag, email, context] << [['-e', '--email'], [userEmail, "notanemail@mail.moc"], [noAuthCtx, ctx]].combinations() + } +} From a5e4f516d206e9cc163c345ad9e069056907ebf5 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Sat, 20 Sep 2025 17:58:05 -0600 Subject: [PATCH 07/10] docs: correct version and author in note --- .../java/org/justserve/cli/command/CommonOptionsMixin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java b/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java index 527e5c0..ce4a634 100644 --- a/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java +++ b/src/main/java/org/justserve/cli/command/CommonOptionsMixin.java @@ -17,8 +17,8 @@ * } * * - * @author Remko Popma - * @version 1.0 + * @author Jonathan Zollinger + * @version 0.0.1 */ @Command(mixinStandardHelpOptions = true, versionProvider = JustServeVersionProvider.class) @SuppressWarnings("checkstyle:VisibilityModifier") From a37cf62deb830f81490bf33f6148e8ad0639df92 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Sat, 20 Sep 2025 17:58:26 -0600 Subject: [PATCH 08/10] build: set gvm to compile with rich output --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 2b5f4b4..de3176c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,6 +78,7 @@ micronaut { graalvmNative.binaries { named("main") { imageName.set("justserve") + buildArgs.add("--color=always") } } From 41af73cc33eebfe487c9efaaf5ba155ba0a90559 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 3 Oct 2025 10:58:47 -0600 Subject: [PATCH 09/10] feat: add DynamicRoutingClient --- src/main/resources/schema.yml | 50 ++++++++++++++++++- .../client/DynamicRoutingClientSpec.groovy | 37 ++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy diff --git a/src/main/resources/schema.yml b/src/main/resources/schema.yml index 94961e2..c9ec7aa 100644 --- a/src/main/resources/schema.yml +++ b/src/main/resources/schema.yml @@ -36,11 +36,44 @@ paths: schema: { $ref: '#/components/schemas/UserSlimSearchResults' } '400': description: Bad Request + content: { application/json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } } '500': description: Internal Server Error + content: { application/json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } } + + /api/v1/routing/{url}: + get: + operationId: getOrgIdFromSlug + tags: + - DynamicRouting + parameters: [ { name: url, in: path, required: true, schema: { type: string } } ] + responses: + '200': + description: OK + content: { application/json: { schema: { $ref: '#/components/schemas/DynamicRoutingDataResponse' } } } + '404': + description: Not Found + content: { application/json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } } components: schemas: + DynamicRoutingDataResponse: + type: object + properties: + id: + type: string + nullable: true + dynamicRouteType: + description: "The type of entity the route points to." + type: integer + format: int32 + enum: + - 0 + - 1 + x-enum-varnames: + - Organization + - DisasterRelief + additionalProperties: false UserHashRequest: description: | A request containing either the email or the userid for a user. @@ -363,4 +396,19 @@ components: "globalAdmin", "globalLeadAdmin", "developer" - ] \ No newline at end of file + ] + ProblemDetails: + type: object + properties: + type: + type: string + format: uri + nullable: true + title: + type: string + nullable: true + status: + type: integer + format: int32 + nullable: true + traceId: { type: string, nullable: true } \ No newline at end of file diff --git a/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy b/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy new file mode 100644 index 0000000..369ba4b --- /dev/null +++ b/src/test/groovy/org/justserve/client/DynamicRoutingClientSpec.groovy @@ -0,0 +1,37 @@ +package org.justserve.client + +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import org.justserve.JustServeSpec +import org.justserve.model.DynamicRoutingDataResponse +import spock.lang.Shared + +class DynamicRoutingClientSpec extends JustServeSpec { + + @Shared + DynamicRoutingClient noAuthClient, authClient + + + def setupSpec() { + noAuthClient = noAuthCtx.getBean(DynamicRoutingClient) + authClient = ctx.getBean(DynamicRoutingClient) + } + + def "get orgId for #url"() { + when: + HttpResponse response = client.getOrgIdFromSlug(url) + + then: + response.status() == expectedStatus + if (expectedStatus == HttpStatus.OK) { + response.body().id != null + } + + where: + url | expectedStatus | client + "blankparkzoo" | HttpStatus.OK | authClient + "blankparkzoo" | HttpStatus.OK | noAuthClient + "1234" | HttpStatus.NOT_FOUND | authClient + "1234" | HttpStatus.NOT_FOUND | noAuthClient + } +} From adf6a4dd990a8897e4e7163bcc271350d64168ad Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Fri, 3 Oct 2025 15:59:49 -0600 Subject: [PATCH 10/10] feat: add getAdminContext --- build.gradle.kts | 1 + src/main/resources/schema.yml | 103 +++++++++++++++++- .../groovy/org/justserve/JustServeSpec.groovy | 5 + .../justserve/client/UserClientSpec.groovy | 34 +++++- 4 files changed, 139 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index de3176c..b3796eb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut.picocli:micronaut-picocli") implementation("io.micronaut.serde:micronaut-serde-jackson") + testImplementation("net.datafaker:datafaker:2.5.1") compileOnly("org.projectlombok:lombok") runtimeOnly("ch.qos.logback:logback-classic") runtimeOnly("org.yaml:snakeyaml") diff --git a/src/main/resources/schema.yml b/src/main/resources/schema.yml index c9ec7aa..6beb725 100644 --- a/src/main/resources/schema.yml +++ b/src/main/resources/schema.yml @@ -21,6 +21,26 @@ paths: description: Bad Request '500': description: Internal Server Error + /api/v1/users/securitycontext/bearer: + get: + description: "Retrieves the admin context for a given user (ie org ID's, church unit id's, role levels, etc). if no user is provided, it will return the context for the currently authenticated user" + operationId: getAdminContext + tags: + - User + parameters: + - name: userId + in: query + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: { application/json: { schema: { $ref: '#/components/schemas/SecurityContext' } } } + '401': # No failures for unfound endpoints. querying ID "this is a bad ID" returns the current user's bearer context. + description: Unauthorized + content: { application/json: { schema: { $ref: '#/components/schemas/ProblemDetails' } } } + /api/v1/users/slimSearch: post: operationId: userSearchSlim @@ -57,6 +77,33 @@ paths: components: schemas: + ChurchCivicGeographyUserRole: + type: object + properties: + churchGeographyId: + type: string + format: uuid + unitId: + type: string + nullable: true + civicGeographyId: + type: string + format: uuid + role: + $ref: '#/components/schemas/Role' + additionalProperties: false + ChurchGeographyUserRole: + type: object + properties: + churchGeographyId: + type: string + format: uuid + unitId: + type: string + nullable: true + role: + $ref: '#/components/schemas/Role' + additionalProperties: false DynamicRoutingDataResponse: type: object properties: @@ -74,6 +121,15 @@ components: - Organization - DisasterRelief additionalProperties: false + OrganizationUserRole: + type: object + properties: + organizationId: + type: string + format: uuid + role: + $ref: '#/components/schemas/Role' + additionalProperties: false UserHashRequest: description: | A request containing either the email or the userid for a user. @@ -358,6 +414,18 @@ components: deletedOn: { type: string, format: date-time, nullable: true } deletedBy: { type: string, format: uuid, nullable: true } additionalProperties: false + OrganizationCivicGeographyUserRole: + type: object + properties: + organizationId: + type: string + format: uuid + role: + $ref: '#/components/schemas/Role' + civicGeographyId: + type: string + format: uuid + additionalProperties: false Location: type: object properties: { latitude: { type: number, format: double }, @@ -411,4 +479,37 @@ components: type: integer format: int32 nullable: true - traceId: { type: string, nullable: true } \ No newline at end of file + traceId: { type: string, nullable: true } + SecurityContext: + type: object + properties: + userId: + type: string + format: uuid + userRepresentativeForOrganizations: + type: array + items: + type: string + format: uuid + nullable: true + churchGeographies: + type: object + additionalProperties: + $ref: '#/components/schemas/ChurchGeographyUserRole' + nullable: true + civicGeographies: + type: object + additionalProperties: + $ref: '#/components/schemas/ChurchCivicGeographyUserRole' + nullable: true + organizationRoles: + type: object + additionalProperties: + $ref: '#/components/schemas/OrganizationUserRole' + nullable: true + organizationCivicGeographyUserRoles: + type: array + items: + $ref: '#/components/schemas/OrganizationCivicGeographyUserRole' + nullable: true + additionalProperties: false \ No newline at end of file diff --git a/src/test/groovy/org/justserve/JustServeSpec.groovy b/src/test/groovy/org/justserve/JustServeSpec.groovy index 75fbe0d..78073b1 100644 --- a/src/test/groovy/org/justserve/JustServeSpec.groovy +++ b/src/test/groovy/org/justserve/JustServeSpec.groovy @@ -3,12 +3,16 @@ package org.justserve import io.micronaut.context.ApplicationContext import io.micronaut.context.env.Environment import io.micronaut.test.extensions.spock.annotation.MicronautTest +import net.datafaker.Faker import spock.lang.Shared import spock.lang.Specification @MicronautTest() class JustServeSpec extends Specification { + @Shared + Faker faker + @Shared ApplicationContext ctx, noAuthCtx @@ -16,6 +20,7 @@ class JustServeSpec extends Specification { String userEmail def setupSpec() { + faker = new Faker() userEmail = "jimmy@justserve.org" if (null != System.getenv("JUSTSERVE_TOKEN")) { diff --git a/src/test/groovy/org/justserve/client/UserClientSpec.groovy b/src/test/groovy/org/justserve/client/UserClientSpec.groovy index 5f0ffbb..166205f 100644 --- a/src/test/groovy/org/justserve/client/UserClientSpec.groovy +++ b/src/test/groovy/org/justserve/client/UserClientSpec.groovy @@ -1,13 +1,13 @@ package org.justserve.client - import io.micronaut.http.HttpResponse -import io.micronaut.http.HttpStatus import io.micronaut.http.client.exceptions.HttpClientResponseException import org.justserve.JustServeSpec import org.justserve.model.UserHashRequestByEmail import spock.lang.Shared +import static io.micronaut.http.HttpStatus.* + class UserClientSpec extends JustServeSpec { @Shared @@ -19,6 +19,34 @@ class UserClientSpec extends JustServeSpec { userClient = ctx.getBean(UserClient) } + def "get admin context for #description"() { + when: + def response = null + def thrownException = null + try { + response = client.getAdminContext(UUID.fromString(uid)) + } catch (HttpClientResponseException e) { + thrownException = e + } + + then: + if (thrownException) { + thrownException.status == expectedStatus + } else { + response.status == expectedStatus + response.body() != null + if (expectedStatus == OK) { + response.body().userId == UUID.fromString(uid) + } + } + + where: + description | uid | client | expectedStatus + "a valid user" | "e23d029c-25f6-4c93-aada-dcbdc6d50c2c" | userClient | OK + "an invalid user" | faker.internet().uuid() | userClient | NOT_FOUND + "a valid user no-auth" | "e23d029c-25f6-4c93-aada-dcbdc6d50c2c" | noAuthUserClient | UNAUTHORIZED + } + def "get tempPassword for #email"() { when: HttpResponse response = null @@ -46,7 +74,7 @@ class UserClientSpec extends JustServeSpec { where: expectedResponse | email | expectedException | expectedMessage | client | _ - HttpStatus.OK | userEmail | null | null | userClient | _ + OK | userEmail | null | null | userClient | _ null | "notanemail@mail.moc" | HttpClientResponseException | "\"status\":500" | userClient | _ null | userEmail | HttpClientResponseException | "\"status\":401" | noAuthUserClient | _ null | "notanemail@mail.moc" | HttpClientResponseException | "\"status\":401" | noAuthUserClient | _