, hyper::http::Error> {
+ if req.method() != hyper::Method::GET {
+ return hyper::Response::builder()
+ .status(hyper::StatusCode::METHOD_NOT_ALLOWED)
+ .header("Allow", "GET")
+ .body("".into());
+ }
+
+ // The authorization code is guaranteed to be sent as a "code" query parameter
+ // in the request URI query string as per RFC 6749 § 4.1.2
+ let auth_code = req.uri().query().and_then(|query_string| {
+ query_string
+ .split('&')
+ .filter_map(|query_pair| query_pair.split_once('='))
+ .find_map(|(key, value)| (key == "code").then_some(value))
+ });
+
+ let response = if let Some(auth_code) = auth_code {
+ *auth_code_out.lock().unwrap() = Some(auth_code.to_string());
+
+ hyper::Response::builder()
+ .status(hyper::StatusCode::OK)
+ .header("Content-Type", "text/html;charset=utf-8")
+ .body(
+ include_str!("auth_code_reply/page.html")
+ .replace("{{title}}", "Success")
+ .replace("{{message}}", "You have successfully signed in! You can close this page now."),
+ )
+ } else {
+ hyper::Response::builder()
+ .status(hyper::StatusCode::BAD_REQUEST)
+ .header("Content-Type", "text/html;charset=utf-8")
+ .body(
+ include_str!("auth_code_reply/page.html")
+ .replace("{{title}}", "Error")
+ .replace("{{message}}", "Authorization code not found. Please try signing in again."),
+ )
+ }?;
+
+ Ok(response)
+}
diff --git a/apps/app/src/api/oauth_utils/auth_code_reply/page.html b/apps/app/src/api/oauth_utils/auth_code_reply/page.html
new file mode 100644
index 00000000..f0ccff4a
--- /dev/null
+++ b/apps/app/src/api/oauth_utils/auth_code_reply/page.html
@@ -0,0 +1 @@
+Sign In - Modrinth App
diff --git a/apps/app/src/api/oauth_utils/mod.rs b/apps/app/src/api/oauth_utils/mod.rs
new file mode 100644
index 00000000..4182cfb6
--- /dev/null
+++ b/apps/app/src/api/oauth_utils/mod.rs
@@ -0,0 +1,3 @@
+//! Assorted utilities for OAuth 2.0 authorization flows.
+
+pub mod auth_code_reply;
diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json
index 06c10baa..78086935 100644
--- a/apps/app/tauri.conf.json
+++ b/apps/app/tauri.conf.json
@@ -63,6 +63,7 @@
"height": 800,
"resizable": true,
"title": "AstralRinth",
+ "label": "main",
"width": 1280,
"minHeight": 700,
"minWidth": 1100,
diff --git a/apps/daedalus_client/Dockerfile b/apps/daedalus_client/Dockerfile
index 9ea70f9c..271c829a 100644
--- a/apps/daedalus_client/Dockerfile
+++ b/apps/daedalus_client/Dockerfile
@@ -1,9 +1,19 @@
+# syntax=docker/dockerfile:1
+
FROM rust:1.88.0 AS build
WORKDIR /usr/src/daedalus
COPY . .
-RUN cargo build --release --package daedalus_client
+RUN --mount=type=cache,target=/usr/src/daedalus/target \
+ --mount=type=cache,target=/usr/local/cargo/git/db \
+ --mount=type=cache,target=/usr/local/cargo/registry \
+ cargo build --release --package daedalus_client
+FROM build AS artifacts
+
+RUN --mount=type=cache,target=/usr/src/daedalus/target \
+ mkdir /daedalus \
+ && cp /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
FROM debian:bookworm-slim
@@ -11,7 +21,7 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates openssl \
&& rm -rf /var/lib/apt/lists/*
-COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
-WORKDIR /daedalus_client
+COPY --from=artifacts /daedalus /daedalus
-CMD /daedalus/daedalus_client
+WORKDIR /daedalus_client
+CMD ["/daedalus/daedalus_client"]
diff --git a/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue b/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue
index 124a4933..cdb97d7c 100644
--- a/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue
+++ b/apps/frontend/src/components/ui/moderation/ModpackPermissionsFlow.vue
@@ -8,7 +8,7 @@
Loading data...
-
All permissions obtained. You may skip this step!
+
All permissions already obtained.
@@ -157,7 +157,7 @@ import type {
} from "@modrinth/utils";
import { ButtonStyled } from "@modrinth/ui";
import { ref, computed, watch, onMounted } from "vue";
-import { useLocalStorage } from "@vueuse/core";
+import { useLocalStorage, useSessionStorage } from "@vueuse/core";
const props = defineProps<{
projectId: string;
@@ -182,7 +182,26 @@ const persistedModPackData = useLocalStorage(
const persistedIndex = useLocalStorage(`modpack-permissions-index-${props.projectId}`, 0);
-const modPackData = ref(null);
+const modPackData = useSessionStorage(
+ `modpack-permissions-data-${props.projectId}`,
+ null,
+ {
+ serializer: {
+ read: (v: any) => (v ? JSON.parse(v) : null),
+ write: (v: any) => JSON.stringify(v),
+ },
+ },
+);
+const permanentNoFiles = useSessionStorage(
+ `modpack-permissions-permanent-no-${props.projectId}`,
+ [],
+ {
+ serializer: {
+ read: (v: any) => (v ? JSON.parse(v) : []),
+ write: (v: any) => JSON.stringify(v),
+ },
+ },
+);
const currentIndex = ref(0);
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
@@ -251,7 +270,45 @@ async function fetchModPackData(): Promise {
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
internal: true,
})) as ModerationModpackResponse;
+
+ const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
+ .filter(([_, file]) => file.status === "permanent-no")
+ .map(
+ ([sha1, file]): ModerationModpackItem => ({
+ sha1,
+ file_name: file.file_name,
+ type: "identified",
+ status: file.status,
+ approved: null,
+ }),
+ )
+ .sort((a, b) => a.file_name.localeCompare(b.file_name));
+
+ permanentNoFiles.value = permanentNoItems;
+
const sortedData: ModerationModpackItem[] = [
+ ...Object.entries(data.identified || {})
+ .filter(
+ ([_, file]) =>
+ file.status !== "yes" &&
+ file.status !== "with-attribution-and-source" &&
+ file.status !== "permanent-no",
+ )
+ .map(
+ ([sha1, file]): ModerationModpackItem => ({
+ sha1,
+ file_name: file.file_name,
+ type: "identified",
+ status: file.status,
+ approved: null,
+ ...(file.status === "unidentified" && {
+ proof: "",
+ url: "",
+ title: "",
+ }),
+ }),
+ )
+ .sort((a, b) => a.file_name.localeCompare(b.file_name)),
...Object.entries(data.unknown_files || {})
.map(
([sha1, fileName]): ModerationUnknownModpackItem => ({
@@ -310,6 +367,7 @@ async function fetchModPackData(): Promise {
} catch (error) {
console.error("Failed to fetch modpack data:", error);
modPackData.value = [];
+ permanentNoFiles.value = [];
persistAll();
}
}
@@ -321,6 +379,14 @@ function goToPrevious(): void {
}
}
+watch(
+ modPackData,
+ (newValue) => {
+ persistedModPackData.value = newValue;
+ },
+ { deep: true },
+);
+
function goToNext(): void {
if (modPackData.value && currentIndex.value < modPackData.value.length) {
currentIndex.value++;
@@ -396,6 +462,17 @@ onMounted(() => {
}
});
+watch(
+ modPackData,
+ (newValue) => {
+ if (newValue && newValue.length === 0) {
+ emit("complete");
+ clearPersistedData();
+ }
+ },
+ { immediate: true },
+);
+
watch(
() => props.projectId,
() => {
@@ -406,6 +483,20 @@ watch(
}
},
);
+
+function getModpackFiles(): {
+ interactive: ModerationModpackItem[];
+ permanentNo: ModerationModpackItem[];
+} {
+ return {
+ interactive: modPackData.value || [],
+ permanentNo: permanentNoFiles.value,
+ };
+}
+
+defineExpose({
+ getModpackFiles,
+});