Show possible alts of user

This commit is contained in:
Nassim Jahnke 2023-05-08 17:31:51 +02:00
parent 4ee03c7c9f
commit c3c98d7326
No known key found for this signature in database
GPG Key ID: 6BE3B555EBC5982B
4 changed files with 45 additions and 3 deletions

View File

@ -7,6 +7,7 @@ import io.papermc.hangar.components.auth.service.CredentialsService;
import io.papermc.hangar.components.auth.service.TokenService;
import io.papermc.hangar.components.images.service.AvatarService;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.dao.internal.table.stats.ProjectViewsDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.requests.RequestPagination;
@ -90,9 +91,10 @@ public class HangarUserController extends HangarComponent {
private final TokenService tokenService;
private final AvatarService avatarService;
private final CredentialsService credentialsService;
private final ProjectViewsDAO projectViewsDAO;
@Autowired
public HangarUserController(final ObjectMapper mapper, final UsersApiService usersApiService, final UserService userService, final NotificationService notificationService, final ProjectRoleService projectRoleService, final OrganizationService organizationService, final OrganizationRoleService organizationRoleService, final ProjectInviteService projectInviteService, final OrganizationInviteService organizationInviteService, final TokenService tokenService, final AvatarService avatarService, final CredentialsService credentialsService) {
public HangarUserController(final ObjectMapper mapper, final UsersApiService usersApiService, final UserService userService, final NotificationService notificationService, final ProjectRoleService projectRoleService, final OrganizationService organizationService, final OrganizationRoleService organizationRoleService, final ProjectInviteService projectInviteService, final OrganizationInviteService organizationInviteService, final TokenService tokenService, final AvatarService avatarService, final CredentialsService credentialsService, final ProjectViewsDAO projectViewsDAO) {
this.mapper = mapper;
this.usersApiService = usersApiService;
this.userService = userService;
@ -105,6 +107,7 @@ public class HangarUserController extends HangarComponent {
this.tokenService = tokenService;
this.avatarService = avatarService;
this.credentialsService = credentialsService;
this.projectViewsDAO = projectViewsDAO;
}
@Anyone
@ -138,6 +141,18 @@ public class HangarUserController extends HangarComponent {
return ResponseEntity.ok(user);
}
@Unlocked
@PermissionRequired(NamedPermission.IS_STAFF)
@ResponseBody
@GetMapping(value = "/users/{userName}/alts", produces = MediaType.APPLICATION_JSON_VALUE)
public List<String> possibleAltAccounts(@PathVariable final String userName) {
final UserTable table = this.userService.getUserTable(userName);
if (table == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown org " + userName);
}
return this.projectViewsDAO.getUsersSharingAddressWith(table.getUserId());
}
// @el(userName: String)
@Unlocked
@CurrentUser("#userName")

View File

@ -2,6 +2,7 @@ package io.papermc.hangar.db.dao.internal.table.stats;
import io.papermc.hangar.model.db.stats.ProjectViewIndividualTable;
import java.net.InetAddress;
import java.util.List;
import java.util.Optional;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
@ -20,4 +21,15 @@ public interface ProjectViewsDAO {
@SqlQuery("SELECT * FROM project_views_individual WHERE address = :address OR (:userId IS NOT NULL AND user_id = :userId) LIMIT 1")
Optional<ProjectViewIndividualTable> getIndividualView(Long userId, InetAddress address);
@SqlQuery("""
SELECT DISTINCT ON (u.id) u.name
FROM users u
JOIN project_views_individual pvi ON u.id = pvi.user_id
WHERE pvi.address IN (
SELECT address FROM project_views_individual WHERE user_id = :userId
)
AND pvi.user_id != :userId;
""")
List<String> getUsersSharingAddressWith(long userId);
}

View File

@ -22,9 +22,10 @@ import { AsyncData } from "nuxt/app";
import { ComputedRef, Ref } from "vue";
import { NuxtApp } from "@nuxt/schema";
import { useApi, useInternalApi } from "~/composables/useApi";
import { ref, useAsyncData, createError } from "#imports";
import { createError, hasPerms, ref, useAsyncData } from "#imports";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useAuthStore } from "~/store/auth";
import { NamedPermission } from "~/types/enums";
export type NonNullAsyncData<T, E = unknown> = { data: Ref<T> } & Pick<AsyncData<T, E>, "pending" | "refresh" | "execute" | "error">;
@ -182,6 +183,7 @@ export async function useUserData(
organizations: { [key: string]: OrganizationRoleTable };
pinned: ProjectCompact[];
organizationVisibility: { [key: string]: boolean } | null;
possibleAlts: string[] | null;
} | null>
> {
const self = user === useAuthStore().user?.name;
@ -198,6 +200,7 @@ export async function useUserData(
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
useApi<ProjectCompact[]>(`users/${user}/pinned`),
self ? useOrgVisibility(user) : null,
hasPerms(NamedPermission.IS_STAFF) ? useInternalApi<string[]>(`users/${user}/alts`) : null,
]);
return {
starred: data[0] as PaginatedResult<ProjectCompact>,
@ -206,6 +209,7 @@ export async function useUserData(
organizations: data[3] as { [key: string]: OrganizationRoleTable },
pinned: data[4] as ProjectCompact[],
organizationVisibility: data[5],
possibleAlts: data[6],
};
})
);

View File

@ -71,7 +71,7 @@ const requestParams = computed(() => {
});
const userData = await useUserData(props.user.name, requestParams.value);
const { starred, watching, organizations, pinned, organizationVisibility } = userData.value || { starred: null };
const { starred, watching, organizations, pinned, organizationVisibility, possibleAlts } = userData.value || { starred: null };
const projects = ref(userData.value?.projects);
const orgRoles = useBackendData.orgRoles.filter((role) => role.assignable);
const authStore = useAuthStore();
@ -167,6 +167,17 @@ useHead(useSeo(props.user.name, description, route, props.user.avatarUrl));
<LockUserModal v-if="!isCurrentUser && hasPerms(NamedPermission.IS_STAFF)" :user="user" />
</Card>
<Card v-if="possibleAlts?.length !== 0" class="mb-4">
<template #header> Shares address with </template>
<ul>
<li v-for="name in possibleAlts" :key="name">
<Link :to="'/' + name">
{{ name }}
</Link>
</li>
</ul>
</Card>
<template v-if="!user.isOrganization && organizations">
<Card class="mb-4" accent>
<template #header>