Improvements and fixes to logs page

This commit is contained in:
Nassim Jahnke 2023-02-14 16:07:54 +01:00
parent 2edd173e25
commit ad4fc8acf6
No known key found for this signature in database
GPG Key ID: 6BE3B555EBC5982B
8 changed files with 69 additions and 16 deletions

View File

@ -76,7 +76,7 @@ public interface IProjectsController {
})
@GetMapping("/projects")
ResponseEntity<PaginatedResult<Project>> getProjects(
@Parameter(description = "Whether projects should be sorted by the relevance to the given query") @RequestParam(defaultValue = "true") boolean relevance,
@Parameter(description = "Whether projects should be sorted by the relevance to the given query") @RequestParam(defaultValue = "true", required = false) boolean relevance,
@Parameter(description = "Pagination information") @NotNull RequestPagination pagination
);

View File

@ -222,8 +222,8 @@ public class BackendDataController {
@GetMapping("/loggedActions")
@Cacheable(CacheConfig.LOGGED_ACTIONS)
@ResponseBody
public Set<String> getLoggedActions() {
return LogAction.LOG_REGISTRY.keySet();
public List<String> getLoggedActions() {
return LogAction.LOG_REGISTRY.keySet().stream().sorted().toList();
}
}

View File

@ -45,7 +45,8 @@ public enum NamedPermission {
RESTORE_PROJECT("restore_project", Permission.DeleteVersion, "RestoreProject"),
HARD_DELETE_PROJECT("hard_delete_project", Permission.HardDeleteProject, "HardDeleteProject"),
HARD_DELETE_VERSION("hard_delete_version", Permission.HardDeleteVersion, "HardDeleteVersion"),
EDIT_ALL_USER_SETTINGS("edit_all_user_settings", Permission.EditAllUserSettings, "EditAllUserSettings");
EDIT_ALL_USER_SETTINGS("edit_all_user_settings", Permission.EditAllUserSettings, "EditAllUserSettings"),
SEE_IP_ADDRESSES("see_ip_addresses", Permission.SeeIPAdresses, "SeeIpAddresses");
private final String value;
private final Permission permission;

View File

@ -57,7 +57,7 @@ public class APIKeyService extends HangarComponent {
final String token = UUID.randomUUID().toString();
final String hashedToken = CryptoUtils.hmacSha256(this.config.security.tokenSecret(), token.getBytes(StandardCharsets.UTF_8));
this.apiKeyDAO.insert(new ApiKeyTable(apiKeyForm.getName(), userIdentified.getUserId(), tokenIdentifier, hashedToken, keyPermission));
this.actionLogger.user(LogAction.USER_APIKEY_CREATED.create(UserContext.of(userIdentified.getUserId()), "Key Name: " + apiKeyForm.getName() + "<br>" + apiKeyForm.getPermissions().stream().map(NamedPermission::getFrontendName).collect(Collectors.joining(",<br>")), ""));
this.actionLogger.user(LogAction.USER_APIKEY_CREATED.create(UserContext.of(userIdentified.getUserId()), "Key '" + apiKeyForm.getName() + "': " + apiKeyForm.getPermissions().stream().map(NamedPermission::getFrontendName).collect(Collectors.joining(", ")), ""));
return tokenIdentifier + "." + token;
}
@ -66,7 +66,7 @@ public class APIKeyService extends HangarComponent {
if (this.apiKeyDAO.delete(keyName, userIdentified.getUserId()) == 0) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
this.actionLogger.user(LogAction.USER_APIKEY_DELETED.create(UserContext.of(userIdentified.getUserId()), "", "Key Name: " + keyName));
this.actionLogger.user(LogAction.USER_APIKEY_DELETED.create(UserContext.of(userIdentified.getUserId()), "", "Key '" + keyName + "'"));
}
}

View File

@ -93,7 +93,7 @@ const slug = computed(() => props.project.namespace.owner + "/" + props.project.
<DropdownItem :to="`/${slug}/notes`">
{{ i18n.t("project.actions.staffNotes", [project.info.noteCount]) }}
</DropdownItem>
<DropdownItem :to="`/admin/log/?projectFilter=/${slug}`">
<DropdownItem :to="`/admin/log?projectFilter=/${slug}`">
{{ i18n.t("project.actions.userActionLogs") }}
</DropdownItem>
<DropdownItem v-if="project.topicId" :href="forumUrl(project.topicId)">

@ -1 +1 @@
Subproject commit e6e51d36f08276d341a6a8eebc44426848405128
Subproject commit ffcce32128af5973c3e4d2e210355dacf9d99b1e

View File

@ -2,9 +2,9 @@
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useHead } from "@vueuse/head";
import { PaginatedResult } from "hangar-api";
import { PaginatedResult, Project, User } from "hangar-api";
import { computed, ref } from "vue";
import { LoggedAction, LoggedActionType } from "hangar-internal";
import { LoggedAction } from "hangar-internal";
import { debounce } from "lodash-es";
import PageTitle from "~/lib/components/design/PageTitle.vue";
import { useActionLogs } from "~/composables/useApiHelper";
@ -15,11 +15,13 @@ import MarkdownModal from "~/components/modals/MarkdownModal.vue";
import DiffModal from "~/components/modals/DiffModal.vue";
import Button from "~/lib/components/design/Button.vue";
import { useSeo } from "~/composables/useSeo";
import { definePageMeta, useInternalApi, watch } from "#imports";
import { definePageMeta, hasPerms, useApi, useInternalApi, useRouter, watch } from "#imports";
import InputText from "~/lib/components/ui/InputText.vue";
import InputSelect from "~/lib/components/ui/InputSelect.vue";
import { useBackendData } from "~/store/backendData";
import { Header } from "~/types/components/SortableTable";
import { NamedPermission } from "~/types/enums";
import InputAutocomplete from "~/lib/components/ui/InputAutocomplete.vue";
definePageMeta({
globalPermsRequired: ["VIEW_LOGS"],
@ -40,14 +42,19 @@ const headers: Header[] = [
{ title: i18n.t("userActionLog.newState"), name: "newState", sortable: false },
];
if (!hasPerms(NamedPermission.SEE_IP_ADDRESSES)) {
headers.splice(1, 1);
}
const page = ref(0);
const sort = ref<string[]>([]);
const filter = ref<{
user?: string;
logAction?: LoggedActionType;
logAction?: string;
authorName?: string;
projectSlug?: string;
}>({});
const requestParams = computed(() => {
const limit = 25;
return {
@ -78,8 +85,45 @@ async function updatePage(newPage: number) {
await update();
}
const router = useRouter();
async function update() {
loggedActions.value = await useInternalApi<PaginatedResult<LoggedAction>>("admin/log/", "GET", requestParams.value);
loggedActions.value = await useInternalApi<PaginatedResult<LoggedAction>>("admin/log", "GET", requestParams.value);
const { limit, offset, ...paramsWithoutLimit } = requestParams.value;
await router.replace({ query: { ...paramsWithoutLimit } });
}
const userSearchResult = ref<string[]>([]);
const authorSearchResult = ref<string[]>([]);
const projectSearchResult = ref<string[]>([]);
async function searchUser(val: string) {
userSearchResult.value = [];
const users = await useApi<PaginatedResult<User>>("users", "get", {
query: val,
limit: 25,
offset: 0,
});
userSearchResult.value = users.result.filter((u) => !u.isOrganization).map((u) => u.name);
}
async function searchAuthor(val: string) {
authorSearchResult.value = [];
const authors = await useApi<PaginatedResult<User>>("users", "get", {
query: val,
limit: 25,
offset: 0,
});
authorSearchResult.value = authors.result.map((u) => u.name);
}
async function searchProject(val: string) {
projectSearchResult.value = [];
const projects = await useApi<PaginatedResult<Project>>("projects", "get", {
q: val,
limit: 25,
offset: 0,
});
projectSearchResult.value = projects.result.map((u) => u.namespace.slug);
}
useHead(useSeo(i18n.t("userActionLog.title"), null, route, null));
@ -91,16 +135,23 @@ useHead(useSeo(i18n.t("userActionLog.title"), null, route, null));
<Card>
<div class="flex mb-4 gap-2">
<div class="basis-3/12">
<InputText v-model="filter.user" label="User" />
<InputAutocomplete
id="userfilter"
v-model="filter.user"
:values="userSearchResult"
:label="i18n.t('organization.settings.transferModal.transferTo')"
@search="searchUser"
/>
</div>
<div class="basis-3/12">
<InputSelect v-model="filter.logAction" :values="useBackendData.loggedActions" label="Action" />
<span v-if="filter.logAction" class="flex justify-center" cursor="pointer" @click="filter.logAction = undefined"> Clear selected action </span>
</div>
<div class="basis-3/12">
<InputText v-model="filter.authorName" label="Author Name" />
<InputAutocomplete id="authorfilter" v-model="filter.authorName" :values="authorSearchResult" label="Author Name" @search="searchAuthor" />
</div>
<div class="basis-3/12">
<InputText v-model="filter.projectSlug" label="Project Slug" />
<InputAutocomplete id="projectfilter" v-model="filter.projectSlug" :values="projectSearchResult" label="Project Slug" @search="searchProject" />
</div>
</div>

View File

@ -76,6 +76,7 @@ export enum NamedPermission {
HARD_DELETE_PROJECT = "hard_delete_project",
HARD_DELETE_VERSION = "hard_delete_version",
EDIT_ALL_USER_SETTINGS = "edit_all_user_settings",
SEE_IP_ADDRESSES = "see_ip_addresses",
}
export enum Platform {