work on project flags

This commit is contained in:
Jake Potrebic 2021-03-27 20:17:53 -07:00
parent 70709c0873
commit 783cde2c87
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
19 changed files with 188 additions and 451 deletions

View File

@ -41,7 +41,7 @@ export default class FlagModal extends mixins(HangarFormModal, HangarProjectMixi
this.loading = true;
this.$api
.requestInternal('flags/', true, 'POST', {
project_id: this.project.id,
projectId: this.project.id,
reason: this.form.selection,
comment: this.form.comment,
})

View File

@ -103,6 +103,17 @@ const msgs: LocaleMessageObject = {
flag: {
flagProject: 'Flag {0}?',
flagSend: 'Successfully flagged, thanks for making this community a better place!',
flags: {
inappropriateContent: 'Inappropriate Content',
impersonation: 'Impersonation or Deception',
spam: 'Spam',
malIntent: 'Malicious Intent',
other: 'Other',
},
error: {
alreadyOpen: 'You can only have 1 unresolved flag on a project',
alreadyResolved: 'This flag is already resolved',
},
},
tabs: {
docs: 'Docs',
@ -592,7 +603,8 @@ const msgs: LocaleMessageObject = {
markResolved: 'Mark resolved',
visibilityActions: 'Visibility actions',
line1: '{0} reported {1} on {2}',
line2: 'Reason: {0}, Comment: {1}',
line2: 'Reason: {0}',
line3: 'Comment: {0}',
},
userAdmin: {
title: 'Edit User',

View File

@ -5,35 +5,33 @@
<NuxtLink :to="'/' + project.namespace.owner + '/' + project.namespace.slug">{{ project.namespace.owner + '/' + project.namespace.slug }}</NuxtLink>
</v-card-title>
<v-card-text>
<v-data-table v-if="flags && flags.length > 0" :headers="headers" :items="flags" disable-filtering disable-sort hide-default-footer>
<v-data-table :loading="$fetchState.pending" :headers="headers" :items="flags" disable-filtering disable-sort hide-default-footer>
<template #no-data>
<v-alert type="info" class="mt-2">{{ $t('flags.noFlags') }}</v-alert>
</template>
<template #item.user="{ item }">{{ item.reportedByName }}</template>
<template #item.reason="{ item }">{{ item.reason }}</template>
<template #item.reason="{ item }">{{ $t(item.reason) }}</template>
<template #item.createdAt="{ item }">{{ $util.prettyDateTime(item.createdAt) }}</template>
<template #item.resolved="{ item }">
<span v-if="item.resolved">{{ $t('flags.resolved', [item.resolvedByName, $util.prettyDate(item.resolvedAt)]) }}</span>
<span v-else v-text="$t('flags.notResolved')"></span>
</template>
</v-data-table>
<v-alert v-else type="info" prominent>{{ $t('flags.noFlags') }}</v-alert>
</v-card-text>
</v-card>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'nuxt-property-decorator';
import { Project } from 'hangar-api';
import { Component } from 'nuxt-property-decorator';
import { Flag } from 'hangar-internal';
import { Context } from '@nuxt/types';
import { GlobalPermission } from '~/utils/perms';
import { NamedPermission } from '~/types/enums';
import { HangarProjectMixin } from '~/components/mixins';
@Component
@GlobalPermission(NamedPermission.MOD_NOTES_AND_FLAGS)
export default class ProjectFlagsPage extends Vue {
@Prop({ required: true })
project!: Project;
flags!: Flag[];
export default class ProjectFlagsPage extends HangarProjectMixin {
flags: Flag[] = [];
get headers() {
return [
@ -45,9 +43,8 @@ export default class ProjectFlagsPage extends Vue {
];
}
async asyncData({ $api, $util, params }: Context) {
const flags = await $api.requestInternal<Flag[]>(`flags/${params.author}/${params.slug}`, false).catch<any>($util.handlePageRequestError);
return { flags };
async fetch() {
this.flags = (await this.$api.requestInternal<Flag[]>(`flags/${this.project.id}`, false).catch<any>(this.$util.handlePageRequestError)) || [];
}
}
</script>

View File

@ -2,31 +2,50 @@
<v-card>
<v-card-title>{{ $t('flagReview.title') }}</v-card-title>
<v-card-text>
<!-- TODO link to project -->
<v-list v-if="flags.length > 0">
<v-list-item v-for="flag in flags" :key="flag.id">
<v-list-item-avatar>
<UserAvatar :username="flag.reportedByName" clazz="user-avatar-xs"></UserAvatar>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{
$t('flagReview.line1', [flag.reportedByName, flag.projectNamespace, $util.prettyDateTime(flag.createdAt)])
}}</v-list-item-title>
<v-list-item-subtitle>{{ $t('flagReview.line2', [flag.reason, flag.comment]) }}</v-list-item-subtitle>
<v-list-item-title>
{{
$t('flagReview.line1', [
flag.reportedByName,
`${flag.projectNamespace.owner}/${flag.projectNamespace.slug}`,
$util.prettyDateTime(flag.createdAt),
])
}}
</v-list-item-title>
<v-list-item-subtitle>{{ $t('flagReview.line2', [$t(flag.reason)]) }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ $t('flagReview.line3', [flag.comment]) }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn small :href="$util.forumUrl(flag.reportedByName)"><v-icon>mdi-reply</v-icon>{{ $t('flagReview.msgUser') }}</v-btn>
<v-btn small :href="$util.forumUrl(flag.projectOwnerName)"><v-icon>mdi-reply</v-icon>{{ $t('flagReview.msgProjectOwner') }}</v-btn>
<v-btn small :href="$util.forumUrl(flag.reportedByName)" class="mr-1">
<v-icon small left>mdi-reply</v-icon>
{{ $t('flagReview.msgUser') }}
</v-btn>
<v-btn small :href="$util.forumUrl(flag.projectNamespace.owner)" class="mr-1">
<v-icon small left>mdi-reply</v-icon>
{{ $t('flagReview.msgProjectOwner') }}
</v-btn>
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-btn small v-bind="attrs" v-on="on"><v-icon>mdi-eye</v-icon>{{ $t('flagReview.visibilityActions') }}</v-btn>
<v-btn small v-bind="attrs" color="warning" class="mr-1" v-on="on">
<v-icon small left>mdi-eye</v-icon>
{{ $t('flagReview.visibilityActions') }}
</v-btn>
</template>
<v-list>
<v-list-item v-for="(v, index) in visibilities" :key="index">
<v-list-item-title @click="visibility(flag, v)">{{ v }}</v-list-item-title>
<v-list-item v-for="(v, index) in visibilities" :key="index" @click="visibility(flag, v)">
<v-list-item-title>{{ v }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn small color="primary" @click="resolve(flag)"><v-icon>mdi-check</v-icon>{{ $t('flagReview.markResolved') }}</v-btn>
<v-btn small color="success" :loading="loading[flag.id]" @click="resolve(flag)"
><v-icon small left>mdi-check</v-icon>{{ $t('flagReview.markResolved') }}</v-btn
>
</v-list-item-action>
</v-list-item>
</v-list>
@ -49,11 +68,7 @@ import { GlobalPermission } from '~/utils/perms';
@GlobalPermission(NamedPermission.MOD_NOTES_AND_FLAGS)
export default class AdminFlagsPage extends Vue {
flags!: Flag[];
async asyncData({ $api, $util }: Context) {
const flags = await $api.requestInternal<Flag[]>(`flags/`, false).catch<any>($util.handlePageRequestError);
return { flags };
}
loading: { [key: number]: boolean } = {};
get visibilities(): Visibility[] {
return Object.keys(Visibility) as Visibility[];
@ -65,13 +80,29 @@ export default class AdminFlagsPage extends Vue {
}
resolve(flag: Flag) {
this.loading[flag.id] = true;
this.$api
.requestInternal<Flag[]>(`flags/${flag.id}/resolve/true`, false, 'POST')
.catch<any>(this.$util.handlePageRequestError)
.catch<any>(this.$util.handleRequestError)
.then(() => {
this.flags = this.flags.filter((f) => f.id !== flag.id);
this.$nuxt.refresh();
this.$auth.refreshUser();
})
.finally(() => {
this.loading[flag.id] = false;
});
}
created() {
for (const flag of this.flags) {
this.loading[flag.id] = false;
}
}
async asyncData({ $api, $util }: Context) {
const flags = await $api.requestInternal<Flag[]>(`flags/`, false).catch<any>($util.handlePageRequestError);
return { flags };
}
}
</script>

View File

@ -1,6 +1,6 @@
declare module 'hangar-internal' {
import { FlagReason, Joinable, Table } from 'hangar-internal';
import { Project } from 'hangar-api';
import { Joinable, Table } from 'hangar-internal';
import { Project, ProjectNamespace } from 'hangar-api';
import { ProjectCategory, Visibility } from '~/types/enums';
interface ProjectOwner {
@ -42,18 +42,16 @@ declare module 'hangar-internal' {
}
interface Flag extends Table {
userId: number;
reportedByName: string;
reason: FlagReason;
isResolved: boolean;
comment: string;
resolvedAt: string;
resolvedBy: number;
resolvedByName: string;
projectId: number;
projectOwnerName: string;
projectSlug: string;
projectNamespace: string;
userId: number; //
reportedByName: string; //
reason: string; //
resolved: boolean; //
comment: string; //
resolvedAt: string | null; //
resolvedBy: number | null; //
resolvedByName: string | null; //
projectId: number; //
projectNamespace: ProjectNamespace;
projectVisibility: Visibility;
}

View File

@ -32,8 +32,8 @@ public class FlagController extends HangarController {
this.flagService = flagService;
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
public void flag(@RequestBody @Valid FlagForm form) {
flagService.createFlag(form.getProjectId(), form.getReason(), form.getComment());
}
@ -46,14 +46,14 @@ public class FlagController extends HangarController {
}
@ResponseBody
@GetMapping(value = "/{author}/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
@GetMapping(path = "/{projectId}", produces = MediaType.APPLICATION_JSON_VALUE)
@PermissionRequired(perms = NamedPermission.MOD_NOTES_AND_FLAGS)
public List<HangarProjectFlag> getFlags(@PathVariable String author, @PathVariable String slug) {
return flagService.getFlags(author, slug);
public List<HangarProjectFlag> getFlags(@PathVariable long projectId) {
return flagService.getFlags(projectId);
}
@ResponseBody
@GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
@GetMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE)
@PermissionRequired(perms = NamedPermission.MOD_NOTES_AND_FLAGS)
public List<HangarProjectFlag> getFlags() {
return flagService.getFlags();

View File

@ -7,8 +7,6 @@ import com.vladsch.flexmark.ext.admonition.AdmonitionExtension;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.controllerold.forms.UserAdminForm;
import io.papermc.hangar.controllerold.util.StatusZ;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectContext;
import io.papermc.hangar.db.customtypes.RoleCategory;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.daoold.PlatformVersionsDao;
@ -25,7 +23,6 @@ import io.papermc.hangar.modelold.Role;
import io.papermc.hangar.modelold.viewhelpers.Activity;
import io.papermc.hangar.modelold.viewhelpers.LoggedActionViewModel;
import io.papermc.hangar.modelold.viewhelpers.OrganizationData;
import io.papermc.hangar.modelold.viewhelpers.ProjectFlag;
import io.papermc.hangar.modelold.viewhelpers.ReviewQueueEntry;
import io.papermc.hangar.modelold.viewhelpers.UnhealthyProject;
import io.papermc.hangar.modelold.viewhelpers.UserData;
@ -38,7 +35,6 @@ import io.papermc.hangar.serviceold.StatsService;
import io.papermc.hangar.serviceold.UserActionLogService;
import io.papermc.hangar.serviceold.UserService;
import io.papermc.hangar.serviceold.VersionService;
import io.papermc.hangar.serviceold.project.FlagService;
import io.papermc.hangar.serviceold.project.ProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
@ -75,7 +71,6 @@ public class ApplicationController extends HangarController {
private final HangarDao<PlatformVersionsDao> platformVersionsDao;
private final UserService userService;
private final ProjectService projectService;
private final FlagService flagService;
private final OrgService orgService;
private final UserActionLogService userActionLogService;
private final VersionService versionService;
@ -91,12 +86,11 @@ public class ApplicationController extends HangarController {
private final Supplier<UserData> userData;
@Autowired
public ApplicationController(HangarDao<PlatformVersionsDao> platformVersionsDao, UserService userService, ProjectService projectService, OrgService orgService, VersionService versionService, FlagService flagService, UserActionLogService userActionLogService, JobService jobService, SitemapService sitemapService, StatsService statsService, RoleService roleService, StatusZ statusZ, ObjectMapper mapper, HangarConfig hangarConfig, HttpServletRequest request, Supplier<UserData> userData) {
public ApplicationController(HangarDao<PlatformVersionsDao> platformVersionsDao, UserService userService, ProjectService projectService, OrgService orgService, VersionService versionService, UserActionLogService userActionLogService, JobService jobService, SitemapService sitemapService, StatsService statsService, RoleService roleService, StatusZ statusZ, ObjectMapper mapper, HangarConfig hangarConfig, HttpServletRequest request, Supplier<UserData> userData) {
this.platformVersionsDao = platformVersionsDao;
this.userService = userService;
this.projectService = projectService;
this.orgService = orgService;
this.flagService = flagService;
this.userActionLogService = userActionLogService;
this.versionService = versionService;
this.jobService = jobService;
@ -151,31 +145,6 @@ public class ApplicationController extends HangarController {
return fillModel(mv);
}
@GlobalPermission(NamedPermission.MOD_NOTES_AND_FLAGS)
@Secured("ROLE_USER")
@GetMapping("/admin/flags")
public ModelAndView showFlags() {
ModelAndView mav = new ModelAndView("users/admin/flags");
mav.addObject("flags", flagService.getAllProjectFlags());
return fillModel(mav);
}
@GlobalPermission(NamedPermission.MOD_NOTES_AND_FLAGS)
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.OK)
@GetMapping("/admin/flags/{id}/resolve/{resolved}")
public void setFlagResolved(@PathVariable long id, @PathVariable boolean resolved) {
ProjectFlag flag = flagService.getProjectFlag(id);
if (flag == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
if (flag.getFlag().getIsResolved() == resolved) return; // No change
flagService.markAsResolved(id, resolved);
String userName = getCurrentUser().getName();
userActionLogService.project(request, LoggedActionType.PROJECT_FLAG_RESOLVED.with(ProjectContext.of(flag.getFlag().getProjectId())), "Flag resovled by " + userName, "Flagged by " + flag.getReportedBy());
}
@GlobalPermission(NamedPermission.VIEW_HEALTH)
@Secured("ROLE_USER")
@GetMapping("/admin/health")

View File

@ -1,16 +1,12 @@
package io.papermc.hangar.controllerold;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectContext;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.daoold.ProjectDao;
import io.papermc.hangar.db.modelold.OrganizationsTable;
import io.papermc.hangar.db.modelold.ProjectsTable;
import io.papermc.hangar.db.modelold.UserProjectRolesTable;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.common.projects.FlagReason;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.modelold.viewhelpers.ProjectData;
import io.papermc.hangar.modelold.viewhelpers.ScopedOrganizationData;
@ -22,8 +18,6 @@ import io.papermc.hangar.serviceold.OrgService;
import io.papermc.hangar.serviceold.RoleService;
import io.papermc.hangar.serviceold.StatsService;
import io.papermc.hangar.serviceold.UserActionLogService;
import io.papermc.hangar.serviceold.UserService;
import io.papermc.hangar.serviceold.project.FlagService;
import io.papermc.hangar.serviceold.project.ProjectFactory;
import io.papermc.hangar.serviceold.project.ProjectService;
import io.papermc.hangar.util.AlertUtil;
@ -55,33 +49,25 @@ public class ProjectsController extends HangarController {
private static final String STATUS_ACCEPT = "accept";
private static final String STATUS_UNACCEPT = "unaccept";
private final HangarConfig hangarConfig;
private final UserService userService;
private final OrgService orgService;
private final FlagService flagService;
private final ProjectService projectService;
private final ProjectFactory projectFactory;
private final RoleService roleService;
private final UserActionLogService userActionLogService;
private final StatsService statsService;
private final HangarDao<ProjectDao> projectDao;
private final HttpServletRequest request;
private final Supplier<ProjectsTable> projectsTable;
private final Supplier<ProjectData> projectData;
@Autowired
public ProjectsController(HangarConfig hangarConfig, UserService userService, OrgService orgService, FlagService flagService, ProjectService projectService, ProjectFactory projectFactory, RoleService roleService, UserActionLogService userActionLogService, StatsService statsService, HangarDao<ProjectDao> projectDao, HttpServletRequest request, Supplier<ProjectsTable> projectsTable, Supplier<ProjectData> projectData) {
this.hangarConfig = hangarConfig;
this.userService = userService;
public ProjectsController(OrgService orgService, ProjectService projectService, ProjectFactory projectFactory, RoleService roleService, UserActionLogService userActionLogService, StatsService statsService, HttpServletRequest request, Supplier<ProjectsTable> projectsTable, Supplier<ProjectData> projectData) {
this.orgService = orgService;
this.flagService = flagService;
this.projectService = projectService;
this.projectFactory = projectFactory;
this.roleService = roleService;
this.userActionLogService = userActionLogService;
this.statsService = statsService;
this.projectDao = projectDao;
this.request = request;
this.projectsTable = projectsTable;
this.projectData = projectData;
@ -140,30 +126,6 @@ public class ProjectsController extends HangarController {
return null; // TODO implement postDiscussionReply request controller
}
@Secured("ROLE_USER")
@PostMapping(value = "/{author}/{slug}/flag", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ModelAndView flag(@PathVariable String author, @PathVariable String slug, @RequestParam("flag-reason") FlagReason flagReason, @RequestParam String comment) {
ProjectsTable project = projectsTable.get();
if (flagService.hasUnresolvedFlag(project.getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Only 1 flag at a time per project per user");
} else if (comment.isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Comment must not be blank");
}
flagService.flagProject(project.getId(), flagReason, comment);
String userName = getCurrentUser().getName();
userActionLogService.project(request, LoggedActionType.PROJECT_FLAGGED.with(ProjectContext.of(project.getId())), "Flagged by " + userName, "Not flagged by " + userName);
return Routes.PROJECTS_SHOW.getRedirect(author, slug); // TODO flashing
}
@GlobalPermission(NamedPermission.MOD_NOTES_AND_FLAGS)
@Secured("ROLE_USER")
@GetMapping("/{author}/{slug}/flags")
public ModelAndView showFlags(@PathVariable String author, @PathVariable String slug) {
ModelAndView mav = new ModelAndView("projects/admin/flags");
mav.addObject("p", projectData.get());
return fillModel(mav);
}
@ProjectPermission(NamedPermission.DELETE_PROJECT)
@UserLock(route = Routes.PROJECTS_SHOW, args = "{#author, #slug}")
@Secured("ROLE_USER")

View File

@ -1,38 +1,35 @@
package io.papermc.hangar.db.dao.internal.projects;
import io.papermc.hangar.model.db.projects.ProjectFlagTable;
import io.papermc.hangar.model.internal.projects.HangarProjectFlag;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.Timestamped;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.springframework.stereotype.Repository;
import java.time.OffsetDateTime;
import java.util.List;
@Repository
@RegisterConstructorMapper(HangarProjectFlag.class)
public interface HangarProjectFlagsDAO {
@Timestamped
@GetGeneratedKeys
@RegisterConstructorMapper(ProjectFlagTable.class)
@SqlUpdate("INSERT INTO project_flags (created_at, project_id, user_id, reason, comment) VALUES (:now, :projectId, :userId, :reason, :comment)")
ProjectFlagTable insert(@BindBean ProjectFlagTable projectFlagsTable);
@RegisterConstructorMapper(HangarProjectFlag.class)
@SqlQuery("SELECT pf.*, fu.name reported_by_name, ru.name resolved_by_name, p.owner_name project_owner_name, p.slug project_slug, p.visibility project_visibility " +
"FROM project_flags pf " +
" JOIN projects p ON pf.project_id = p.id " +
" JOIN users fu ON pf.user_id = fu.id " +
" LEFT JOIN users ru ON ru.id = pf.resolved_by " +
"WHERE pf.id = :flagId " +
"GROUP BY pf.id, fu.id, ru.id, p.id")
HangarProjectFlag getById(long flagId);
@SqlQuery("SELECT pf.*, fu.name reported_by_name, ru.name resolved_by_name, p.owner_name project_owner_name, p.slug project_slug, p.visibility project_visibility " +
"FROM project_flags pf " +
" JOIN projects p ON pf.project_id = p.id " +
" JOIN users fu ON pf.user_id = fu.id " +
" LEFT OUTER JOIN users ru ON ru.id = pf.resolved_by " +
"WHERE lower(p.owner_name) = lower(:author) AND lower(p.slug) = lower(:slug)" +
"WHERE pf.project_id = :projectId " +
"GROUP BY pf.id, fu.id, ru.id, p.id")
List<HangarProjectFlag> getFlags(String author, String slug);
List<HangarProjectFlag> getFlags(long projectId);
@RegisterConstructorMapper(HangarProjectFlag.class)
@SqlQuery("SELECT pf.*, fu.name reported_by_name, ru.name resolved_by_name, p.owner_name project_owner_name, p.slug project_slug, p.visibility project_visibility " +
"FROM project_flags pf " +
" JOIN projects p ON pf.project_id = p.id " +
@ -41,9 +38,4 @@ public interface HangarProjectFlagsDAO {
"WHERE NOT pf.resolved " +
"GROUP BY pf.id, fu.id, ru.id, p.id")
List<HangarProjectFlag> getFlags();
@SqlUpdate("UPDATE project_flags SET resolved = :resolved, resolved_by = :resolvedBy, resolved_at = :resolvedAt WHERE id = :flagId")
@GetGeneratedKeys
@RegisterConstructorMapper(ProjectFlagTable.class)
ProjectFlagTable markAsResolved(long flagId, boolean resolved, Long resolvedBy, OffsetDateTime resolvedAt);
}

View File

@ -0,0 +1,28 @@
package io.papermc.hangar.db.dao.internal.table.projects;
import io.papermc.hangar.model.db.projects.ProjectFlagTable;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.Timestamped;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.springframework.stereotype.Repository;
import java.time.OffsetDateTime;
@Repository
@RegisterConstructorMapper(ProjectFlagTable.class)
public interface ProjectFlagsDAO {
@Timestamped
@GetGeneratedKeys
@SqlUpdate("INSERT INTO project_flags (created_at, project_id, user_id, reason, comment) VALUES (:now, :projectId, :userId, :reason, :comment)")
ProjectFlagTable insert(@BindBean ProjectFlagTable projectFlagsTable);
@SqlUpdate("UPDATE project_flags SET resolved = :resolved, resolved_by = :resolvedBy, resolved_at = :resolvedAt WHERE id = :flagId")
void markAsResolved(long flagId, boolean resolved, Long resolvedBy, OffsetDateTime resolvedAt);
@SqlQuery("SELECT * FROM project_flags WHERE project_id = :projectId AND user_id = :userId AND resolved IS FALSE")
ProjectFlagTable getUnresolvedFlag(long projectId, long userId);
}

View File

@ -1,69 +0,0 @@
package io.papermc.hangar.db.daoold;
import io.papermc.hangar.modelold.viewhelpers.ProjectFlag;
import io.papermc.hangar.db.modelold.ProjectFlagsTable;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.Timestamped;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.springframework.stereotype.Repository;
import java.time.OffsetDateTime;
import java.util.Map;
@Repository
@RegisterBeanMapper(ProjectFlagsTable.class)
public interface FlagDao {
@Timestamped
@GetGeneratedKeys
@SqlUpdate("INSERT INTO project_flags (created_at, project_id, user_id, reason, comment) VALUES (:now, :projectId, :userId, :reason, :comment)")
ProjectFlagsTable insert(@BindBean ProjectFlagsTable projectFlagsTable);
@RegisterBeanMapper(value = ProjectFlagsTable.class, prefix = "p")
@RegisterBeanMapper(value = ProjectFlag.class, prefix = "pf")
@SqlQuery("SELECT pf.id p_id, pf.created_at p_created_at, pf.project_id p_project_id, pf.user_id p_user_id, pf.reason p_reason, pf.is_resolved p_is_resolved, pf.comment p_comment, pf.resolved_at p_resolved_at, pf.resolved_by p_resolved_by, " +
"fu.name pf_reported_by, ru.name pf_resolved_by, p.owner_name pf_project_owner_name, p.slug pf_project_slug, p.visibility pf_project_visibility " +
"FROM project_flags pf " +
" JOIN projects p ON pf.project_id = p.id " +
" JOIN users fu ON pf.user_id = fu.id " +
" LEFT OUTER JOIN users ru ON ru.id = pf.resolved_by " +
"WHERE pf.id = :flagId")
Map<ProjectFlag, ProjectFlagsTable> getById(long flagId);
@SqlUpdate("UPDATE project_flags SET is_resolved = :resolved, resolved_by = :resolvedBy, resolved_at = :resolvedAt WHERE id = :flagId")
@GetGeneratedKeys
ProjectFlagsTable markAsResolved(long flagId, boolean resolved, Long resolvedBy, OffsetDateTime resolvedAt);
@SqlQuery("SELECT * FROM project_flags WHERE user_id = :userId AND project_id = :projectId AND is_resolved = false")
ProjectFlagsTable getUnresolvedFlag(long projectId, long userId);
@RegisterBeanMapper(value = ProjectFlagsTable.class, prefix = "p")
@RegisterBeanMapper(value = ProjectFlag.class, prefix = "pf")
@SqlQuery("SELECT pf.id p_id, pf.created_at p_created_at, pf.project_id p_project_id, pf.user_id p_user_id, pf.reason p_reason, pf.is_resolved p_is_resolved, pf.comment p_comment, pf.resolved_at p_resolved_at, pf.resolved_by p_resolved_by, " +
"u.name pf_reported_by, ru.name pf_resolved_by, p.owner_name pf_project_owner_name, p.slug pf_project_slug, p.visibility pf_project_visibility " +
"FROM project_flags pf " +
" JOIN users u ON u.id = pf.user_id " +
" JOIN projects p ON pf.project_id = p.id" +
" LEFT OUTER JOIN users ru ON ru.id = pf.resolved_by " +
"WHERE pf.project_id = :projectId " +
"ORDER BY pf.created_at")
Map<ProjectFlag, ProjectFlagsTable> getProjectFlags(long projectId);
@RegisterBeanMapper(value = ProjectFlagsTable.class, prefix = "p")
@RegisterBeanMapper(value = ProjectFlag.class, prefix = "pf")
@SqlQuery("SELECT pf.id p_id, pf.created_at p_created_at, pf.project_id p_project_id, pf.user_id p_user_id, pf.reason p_reason, pf.is_resolved p_is_resolved, pf.comment p_comment, pf.resolved_at p_resolved_at, pf.resolved_by p_resolved_by, " +
"fu.name pf_reported_by, ru.name pf_resolved_by, p.owner_name pf_project_owner_name, p.slug pf_project_slug, p.visibility pf_project_visibility " +
"FROM project_flags pf " +
" JOIN projects p ON pf.project_id = p.id " +
" JOIN users fu ON pf.user_id = fu.id " +
" LEFT OUTER JOIN users ru ON ru.id = pf.resolved_by " +
"WHERE NOT pf.is_resolved " +
"GROUP BY pf.id, fu.id, ru.id, p.id")
Map<ProjectFlag, ProjectFlagsTable> getFlags();
}

View File

@ -1,18 +1,22 @@
package io.papermc.hangar.model.common.projects;
import com.fasterxml.jackson.annotation.JsonValue;
public enum FlagReason {
INAPPROPRIATE_CONTENT("Inappropriate Content"),
IMPERSONATION("Impersonation or Deception"),
SPAM("Spam"),
MAL_INTENT("Malicious Intent"),
OTHER("Other");
INAPPROPRIATE_CONTENT("project.flag.flags.inappropriateContent"),
IMPERSONATION("project.flag.flags.impersonation"),
SPAM("project.flag.flags.spam"),
MAL_INTENT("project.flag.flags.malIntent"),
OTHER("project.flag.flags.other");
private final String title;
FlagReason(String title) {
this.title = title;
}
@JsonValue
public String getTitle() {
return title;
}

View File

@ -15,10 +15,10 @@ public class ProjectFlagTable extends Table {
private boolean resolved;
private final String comment;
private OffsetDateTime resolvedAt;
private long resolvedBy;
private Long resolvedBy;
@JdbiConstructor
public ProjectFlagTable(OffsetDateTime createdAt, long id, long projectId, long userId, @EnumByOrdinal FlagReason reason, boolean resolved, String comment, OffsetDateTime resolvedAt, long resolvedBy) {
public ProjectFlagTable(OffsetDateTime createdAt, long id, long projectId, long userId, @EnumByOrdinal FlagReason reason, boolean resolved, String comment, OffsetDateTime resolvedAt, Long resolvedBy) {
super(createdAt, id);
this.projectId = projectId;
this.userId = userId;
@ -69,11 +69,11 @@ public class ProjectFlagTable extends Table {
this.resolvedAt = resolvedAt;
}
public long getResolvedBy() {
public Long getResolvedBy() {
return resolvedBy;
}
public void setResolvedBy(long resolvedBy) {
public void setResolvedBy(Long resolvedBy) {
this.resolvedBy = resolvedBy;
}

View File

@ -1,9 +1,11 @@
package io.papermc.hangar.model.internal.projects;
import io.papermc.hangar.model.api.project.ProjectNamespace;
import io.papermc.hangar.model.common.projects.FlagReason;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.db.projects.ProjectFlagTable;
import org.jdbi.v3.core.enums.EnumByOrdinal;
import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
@ -11,16 +13,14 @@ public class HangarProjectFlag extends ProjectFlagTable {
private final String reportedByName;
private final String resolvedByName;
private final String projectOwnerName;
private final String projectSlug;
private final ProjectNamespace projectNamespace;
private final Visibility projectVisibility;
public HangarProjectFlag(OffsetDateTime createdAt, long id, long projectId, long userId, @EnumByOrdinal FlagReason reason, boolean resolved, String comment, OffsetDateTime resolvedAt, long resolvedBy, String reportedByName, String resolvedByName, String projectOwnerName, String projectSlug, @EnumByOrdinal Visibility projectVisibility) {
public HangarProjectFlag(OffsetDateTime createdAt, long id, long projectId, long userId, @EnumByOrdinal FlagReason reason, boolean resolved, String comment, OffsetDateTime resolvedAt, Long resolvedBy, String reportedByName, @Nullable String resolvedByName, String projectOwnerName, String projectSlug, @EnumByOrdinal Visibility projectVisibility) {
super(createdAt, id, projectId, userId, reason, resolved, comment, resolvedAt, resolvedBy);
this.reportedByName = reportedByName;
this.resolvedByName = resolvedByName;
this.projectOwnerName = projectOwnerName;
this.projectSlug = projectSlug;
this.projectNamespace = new ProjectNamespace(projectOwnerName, projectSlug);
this.projectVisibility = projectVisibility;
}
@ -32,29 +32,20 @@ public class HangarProjectFlag extends ProjectFlagTable {
return resolvedByName;
}
public String getProjectOwnerName() {
return projectOwnerName;
}
public String getProjectSlug() {
return projectSlug;
public ProjectNamespace getProjectNamespace() {
return projectNamespace;
}
public Visibility getProjectVisibility() {
return projectVisibility;
}
public String getProjectNamespace() {
return projectOwnerName + "/" + projectSlug;
}
@Override
public String toString() {
return "HangarProjectFlag{" +
"reportedByName='" + reportedByName + '\'' +
", resolvedByName='" + resolvedByName + '\'' +
", projectOwnerName='" + projectOwnerName + '\'' +
", projectSlug='" + projectSlug + '\'' +
", projectNamespace=" + projectNamespace +
", projectVisibility=" + projectVisibility +
"} " + super.toString();
}

View File

@ -1,11 +1,17 @@
package io.papermc.hangar.service.internal.admin;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectContext;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.projects.HangarProjectFlagsDAO;
import io.papermc.hangar.db.dao.internal.table.projects.ProjectFlagsDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.projects.FlagReason;
import io.papermc.hangar.model.db.projects.ProjectFlagTable;
import io.papermc.hangar.model.internal.projects.HangarProjectFlag;
import io.papermc.hangar.service.HangarService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
@ -14,25 +20,43 @@ import java.util.List;
@Service
public class FlagService extends HangarService {
private final ProjectFlagsDAO projectFlagsDAO;
private final HangarProjectFlagsDAO hangarProjectFlagsDAO;
public FlagService(HangarDao<HangarProjectFlagsDAO> hangarProjectFlagsDAO) {
@Autowired
public FlagService(HangarDao<ProjectFlagsDAO> projectFlagsDAO, HangarDao<HangarProjectFlagsDAO> hangarProjectFlagsDAO) {
this.projectFlagsDAO = projectFlagsDAO.get();
this.hangarProjectFlagsDAO = hangarProjectFlagsDAO.get();
}
public void createFlag(long projectId, FlagReason reason, String comment) {
// TODO idk, we prolly need more checking here, plus notification? logs?
hangarProjectFlagsDAO.insert(new ProjectFlagTable( projectId, getHangarPrincipal().getId(), reason, comment));
if (hasUnresolvedFlag(projectId, getHangarPrincipal().getId())) {
throw new HangarApiException("project.flag.error.alreadyOpen");
}
projectFlagsDAO.insert(new ProjectFlagTable( projectId, getHangarPrincipal().getId(), reason, comment));
userActionLogService.project(LoggedActionType.PROJECT_FLAGGED.with(ProjectContext.of(projectId)), "Flagged by " + getHangarPrincipal().getName(), "");
}
public ProjectFlagTable markAsResolved(long flagId, boolean resolved) {
public boolean hasUnresolvedFlag(long projectId, long userId) {
return projectFlagsDAO.getUnresolvedFlag(projectId, userId) != null;
}
public void markAsResolved(long flagId, boolean resolved) {
HangarProjectFlag hangarProjectFlag = hangarProjectFlagsDAO.getById(flagId);
if (hangarProjectFlag == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
if (hangarProjectFlag.isResolved()) {
throw new HangarApiException("project.flag.error.alreadyResolved");
}
Long resolvedBy = resolved ? getHangarPrincipal().getId() : null;
OffsetDateTime resolvedAt = resolved ? OffsetDateTime.now() : null;
return hangarProjectFlagsDAO.markAsResolved(flagId, resolved, resolvedBy, resolvedAt);
projectFlagsDAO.markAsResolved(flagId, resolved, resolvedBy, resolvedAt);
userActionLogService.project(LoggedActionType.PROJECT_FLAG_RESOLVED.with(ProjectContext.of(hangarProjectFlag.getProjectId())), "Flag resolved by " + getHangarPrincipal().getName(), "Flag reported by " + hangarProjectFlag.getReportedByName());
}
public List<HangarProjectFlag> getFlags(String author, String slug) {
return hangarProjectFlagsDAO.getFlags(author, slug);
public List<HangarProjectFlag> getFlags(long projectId) {
return hangarProjectFlagsDAO.getFlags(projectId);
}
public List<HangarProjectFlag> getFlags() {

View File

@ -1,60 +0,0 @@
package io.papermc.hangar.serviceold.project;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.daoold.FlagDao;
import io.papermc.hangar.db.modelold.ProjectFlagsTable;
import io.papermc.hangar.model.common.projects.FlagReason;
import io.papermc.hangar.modelold.viewhelpers.ProjectFlag;
import io.papermc.hangar.serviceold.HangarService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Deprecated(forRemoval = true)
@Service("flagServiceOld")
public class FlagService extends HangarService {
private final HangarDao<FlagDao> flagDao;
@Autowired
public FlagService(HangarDao<FlagDao> flagDao) {
this.flagDao = flagDao;
}
public boolean hasUnresolvedFlag(long projectId) {
return flagDao.get().getUnresolvedFlag(projectId, getCurrentUser().getId()) != null;
}
public void flagProject(long projectId, FlagReason flagReason, String comment) {
ProjectFlagsTable flag = new ProjectFlagsTable(
projectId,
getCurrentUser().getId(),
flagReason,
comment
);
flagDao.get().insert(flag);
}
public ProjectFlagsTable markAsResolved(long flagId, boolean resolved) {
Long resolvedBy = resolved ? getCurrentUser().getId() : null;
OffsetDateTime resolvedAt = resolved ? OffsetDateTime.now() : null;
return flagDao.get().markAsResolved(flagId, resolved, resolvedBy, resolvedAt);
}
public List<ProjectFlag> getProjectFlags(long projectId) {
return flagDao.get().getProjectFlags(projectId).entrySet().stream().map(entry -> entry.getKey().with(entry.getValue())).collect(Collectors.toList());
}
public ProjectFlag getProjectFlag(long flagId) {
List<ProjectFlag> flags = flagDao.get().getById(flagId).entrySet().stream().map(entry -> entry.getKey().with(entry.getValue())).collect(Collectors.toList());
if (flags.size() != 1) return null;
return flags.get(0);
}
public List<ProjectFlag> getAllProjectFlags() {
return flagDao.get().getFlags().entrySet().stream().map(entry -> entry.getKey().with(entry.getValue())).collect(Collectors.toList());
}
}

View File

@ -35,6 +35,7 @@ import org.springframework.web.server.ResponseStatusException;
import javax.servlet.http.HttpServletRequest;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -51,21 +52,19 @@ public class ProjectService extends HangarService {
private final HangarDao<UserDao> userDao;
private final HangarDao<VisibilityDao> visibilityDao;
private final HangarDao<GeneralDao> generalDao;
private final FlagService flagService;
private final PermissionService permissionService;
private final ProjectFiles projectFiles;
private final HttpServletRequest request;
@Autowired
public ProjectService(HangarConfig hangarConfig, HangarDao<ProjectDao> projectDao, HangarDao<UserDao> userDao, HangarDao<VisibilityDao> visibilityDao, HangarDao<GeneralDao> generalDao, ProjectFiles projectFiles, FlagService flagService, PermissionService permissionService, HttpServletRequest request) {
public ProjectService(HangarConfig hangarConfig, HangarDao<ProjectDao> projectDao, HangarDao<UserDao> userDao, HangarDao<VisibilityDao> visibilityDao, HangarDao<GeneralDao> generalDao, ProjectFiles projectFiles, PermissionService permissionService, HttpServletRequest request) {
this.hangarConfig = hangarConfig;
this.projectDao = projectDao;
this.userDao = userDao;
this.visibilityDao = visibilityDao;
this.generalDao = generalDao;
this.projectFiles = projectFiles;
this.flagService = flagService;
this.permissionService = permissionService;
this.request = request;
}
@ -106,7 +105,7 @@ public class ProjectService extends HangarService {
int publicVersions = 0;
Map<UserProjectRolesTable, UsersTable> projectMembers = projectDao.get().getProjectMembers(projectsTable.getId());
List<ProjectFlag> flags = flagService.getProjectFlags(projectsTable.getId());
List<ProjectFlag> flags = /*flagService.getProjectFlags(projectsTable.getId());*/ new ArrayList<>();
ArrayNode messages = (ArrayNode) projectsTable.getNotes().getJson().get("messages");
int noteCount;
if (messages == null) {

View File

@ -1,55 +0,0 @@
<#import "/spring.ftl" as spring />
<#import "*/utils/hangar.ftlh" as hangar />
<#import "*/layout/base.ftlh" as base />
<#assign message><@spring.message "project.flag.plural" /></#assign>
<@base.base title="${message}">
<div class="row">
<div class="col-md-12 header-flags">
<div class="clearfix">
<h1 class="float-left"><@spring.message "project.flag.plural" /> for <a href="${Routes.PROJECTS_SHOW.getRouteUrl(p.project.ownerName, p.project.slug)}">${p.project.ownerName}/${p.project.slug}</a></h1>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<#if p.flagCount == 0>
<div class="alert-review alert alert-info" role="alert">
<i class="fas fa-info-circle"></i>
No flags found
</div>
<#else>
<div class="card">
<div class="card-header">
<h4 class="card-title float-left"><@spring.message "project.flag.plural" /></h4>
<div class="clearfix"></div>
</div>
<table class="table table-sm setting-no-border table-review-log">
<thead>
<tr>
<th>Submitter</th>
<th>Reason</th>
<th>When</th>
<th>Resolved</th>
</tr>
</thead>
<tbody>
<#list p.flags as flag>
<tr>
<td>${flag.reportedBy}</td>
<td>${flag.flag.reason.title}, ${flag.flag.comment}</td>
<td>${utils.prettifyDateTime(flag.flag.createdAt)}</td>
<#if flag.flag.isResolved>
<td>${flag.resolvedBy} at ${utils.prettifyDateTime(flag.flag.resolvedAt)}</td>
<#else>
<td>-not resolved-</td>
</#if>
</tr>
</#list>
</tbody>
</table>
</#if>
</div>
</div>
</div>
</@base.base>

View File

@ -1,86 +0,0 @@
<#import "/spring.ftl" as spring />
<#import "*/utils/hangar.ftlh" as hangar />
<#import "*/layout/base.ftlh" as base />
<#import "*/projects/helper/btnHide.ftlh" as hide />
<#assign scriptsVar>
<script type="text/javascript" src="${hangar.url("js/adminFlags.js")}"></script>
<script type="text/javascript" src="${hangar.url("js/hideProject.js")}"></script>
</#assign>
<@base.base title="Flags" additionalScripts=scriptsVar>
<div class="row">
<div class="col-md-12 header-flags">
<h2>Flags</h2>
<h3 class="minor no-flags" <#if flags?has_content>style="display: none;"</#if>>
<i class="far fa-thumbs-up"></i> <@spring.message "user.flags.none" />
</h3>
</div>
</div>
<div class="row">
<div class="col-md-12">
<ul class="list-group list-flags-admin">
<#list flags as flag>
<li data-flag-id="${flag.flag.id}" class="list-group-item">
<div class="row">
<div class="col-12 col-md-1" style="width: 40px;">
<a href="${Routes.USERS_SHOW_PROJECTS.getRouteUrl(flag.reportedBy)}">
<#import "*/utils/userAvatar.ftlh" as userAvatar>
<@userAvatar.userAvatar userName=flag.reportedBy avatarUrl=utils.format(config.security.api.avatarUrl, flag.reportedBy) clazz="user-avatar-xs"></@userAvatar.userAvatar>
</a>
</div>
<div class="col-12 col-md-11">
<span class="description">
<strong>${flag.reportedBy}</strong>
<span class="minor"> reported </span>
<a href="${Routes.PROJECTS_SHOW.getRouteUrl(flag.projectOwnerName, flag.projectSlug)}">
${flag.projectNamespace}
</a>
<span class="minor"> for </span>
<strong>${flag.flag.reason.title}</strong>
<span class="minor"> at </span>
<strong>${utils.prettifyDateTime(flag.flag.createdAt)}</strong>
<br><i class="minor">${flag.flag.comment}</i>
</span>
</div>
<div class="col-12">
<span class="float-right btn-group-sm">
<a target="_blank" rel="noopener" href="https://papermc.io/forums/users/${flag.reportedBy}" class="btn btn-default">
<i class="fas fa-reply"></i> <@spring.message "user.flags.messageUser" />
</a>
<a target="_blank" rel="noopener" href="https://papermc.io/forums/users/${flag.projectOwnerName}" class="btn btn-default">
<i class="fas fa-reply"></i> <@spring.message "user.flags.messageOwner" />
</a>
<@hide.btnHide flag.projectNamespace flag.projectVisibility />
<button type="submit" class="btn btn-primary btn-resolve">
<i class="fas fa-check"></i> <strong><@spring.message "user.flags.markResolved" /></strong>
</button>
</span>
</div>
</div>
</li>
</#list>
</ul>
</div>
</div>
<div class="modal fade" id="modal-visibility-needschanges" tabindex="-1" role="dialog" aria-labelledby="modal-visibility-needschanges">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="<@spring.message "general.close" />">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Needs Changes</h4>
</div>
<div class="modal-body">
<textarea class="textarea-needschanges form-control" rows="3"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal"><@spring.message "general.close" /></button>
<button class="btn btn-visibility-needschanges-submit btn-primary"><i class="fa fa-pencil-alt"></i> Submit</button>
</div>
</div>
</div>
</div>
</@base.base>