work on version page

This commit is contained in:
Jake Potrebic 2021-03-09 20:07:53 -08:00
parent 2ba0abe3d4
commit 7af8e89398
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
26 changed files with 430 additions and 120 deletions

View File

@ -268,6 +268,7 @@ const msgs: LocaleMessageObject = {
required: '(required)',
adminMsg: '{0} approved this version on {1}',
reviewLogs: 'Review logs',
reviewStart: 'Start review',
delete: 'Delete',
download: 'Download',
downloadExternal: 'Download External',

View File

@ -11,8 +11,8 @@ export default async ({ app: { $cookies }, $auth, $api, store, redirect }: Conte
path: '/',
});
redirect(returnRoute);
}
if ($cookies.get('HangarAuth_REFRESH', { parseJSON: false })) {
// TODO if not running hangarauth locally, this needs to just be a regular if not an else-if (idk what a good fix for that is)
} else if ($cookies.get('HangarAuth_REFRESH', { parseJSON: false })) {
const token = await $api.getToken(true);
if (token != null) {
if (store.state.auth.authenticated) {

View File

@ -58,31 +58,33 @@
<FlagModal :project="project" />
<v-menu v-if="isStaff" bottom offset-y>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on">
<v-btn v-bind="attrs" class="ml-1" v-on="on">
{{ $t('project.actions.adminActions') }}
</v-btn>
</template>
<v-list-item :to="slug + '/flags'">
<v-list-item-title>
{{ $t('project.actions.flagHistory', [project.info.flagCount]) }}
</v-list-item-title>
</v-list-item>
<v-list-item :to="slug + '/notes'">
<v-list-item-title>
{{ $t('project.actions.staffNotes', [project.info.noteCount]) }}
</v-list-item-title>
</v-list-item>
<v-list-item :to="'/admin/log/?projectFilter=' + slug">
<v-list-item-title>
{{ $t('project.actions.userActionLogs') }}
</v-list-item-title>
</v-list-item>
<v-list-item :href="$util.forumUrl(project.namespace.owner)">
<v-list-item-title>
{{ $t('project.actions.forum') }}
<v-icon>mdi-open-in-new</v-icon>
</v-list-item-title>
</v-list-item>
<v-list>
<v-list-item :to="slug + '/flags'" nuxt>
<v-list-item-title>
{{ $t('project.actions.flagHistory', [project.info.flagCount]) }}
</v-list-item-title>
</v-list-item>
<v-list-item :to="slug + '/notes'" nuxt>
<v-list-item-title>
{{ $t('project.actions.staffNotes', [project.info.noteCount]) }}
</v-list-item-title>
</v-list-item>
<v-list-item :to="'/admin/log/?projectFilter=' + slug" nuxt>
<v-list-item-title>
{{ $t('project.actions.userActionLogs') }}
</v-list-item-title>
</v-list-item>
<v-list-item :href="$util.forumUrl(project.namespace.owner)">
<v-list-item-title>
{{ $t('project.actions.forum') }}
<v-icon>mdi-open-in-new</v-icon>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-row>
</v-col>

View File

@ -1,46 +1,60 @@
<template>
<div>
<v-row v-if="version">
<div style="float: left">
<v-col>
<h1>{{ version.name }}</h1>
<TagComponent :tag="channel" :short-form="true" />
<v-subheader>{{ $t('version.page.subheader', [version.author, $util.prettyDate(version.createdAt)]) }}</v-subheader>
</div>
<div style="float: right">
<v-subheader>
<!-- todo approver and stuff, perm -->
<i v-if="true">{{ $t('version.page.adminMsg', [version.author, $util.prettyDate(version.createdAt)]) }}</i>
<!-- todo check if recommended -->
<v-icon v-if="true" :title="$t('version.page.recommended')">mdi-diamond-stone</v-icon>
<v-icon v-if="isChecked" :title="approvalTooltip">mdi-check-circle-outline</v-icon>
</v-col>
<v-col class="text-right">
<v-subheader style="justify-content: end">
<i v-if="isReviewer && version.approvedBy">{{ $t('version.page.adminMsg', [version.approvedBy, $util.prettyDate(version.createdAt)]) }}</i>
<v-icon v-if="version.recommended" :title="$t('version.page.recommended')">mdi-diamond-stone</v-icon>
<v-icon v-if="isReviewStateChecked" :title="approvalTooltip">mdi-check-circle-outline</v-icon>
</v-subheader>
<!-- todo perms -->
<v-btn color="secondary" :to="$route.path + '/reviews'">{{ $t('version.page.reviewLogs') }}</v-btn>
<v-btn color="error" @click="deleteVersion">{{ $t('version.page.delete') }}</v-btn>
<!-- todo check recommended -->
<v-btn v-if="true" color="primary" :to="$route.path + '/download'">{{ $t('version.page.download') }}</v-btn>
<!-- todo maybe move the review logs to the admin actions dropdown? -->
<template v-if="isReviewer">
<v-btn v-if="isReviewStateChecked" color="secondary" :to="$route.path + '/reviews'" nuxt>{{ $t('version.page.reviewLogs') }}</v-btn>
<v-btn v-else color="secondary" :to="$route.path + '/reviews'" nuxt>
<v-icon left>mdi-play</v-icon>
{{ $t('version.page.reviewStart') }}
</v-btn>
</template>
<v-btn v-if="canDeleteVersion" color="error" @click="deleteVersion">{{ $t('version.page.delete') }}</v-btn>
<v-btn v-if="!version.externalUrl" color="primary" :to="$route.path + '/download'">{{ $t('version.page.download') }}</v-btn>
<v-btn v-else color="primary" :to="$route.path + '/download'">{{ $t('version.page.downloadExternal') }}</v-btn>
<!-- todo perms -->
<v-menu offset-y>
<v-menu v-if="canViewLogs || isReviewer || canHardDeleteVersion" offset-y open-on-hover>
<template #activator="{ on, attrs }">
<v-btn plain dark v-bind="attrs" v-on="on">{{ $t('version.page.adminActions') }}</v-btn>
<v-btn v-ripple="false" plain v-bind="attrs" v-on="on">
{{ $t('version.page.adminActions') }}
<v-icon right>mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item>
<!--todo route for user action log-->
<v-list-item nuxt :to="`ddd`">
<v-list-item-title>
<nuxt-link to="ddd" class="text-decoration-none">
{{ $t('version.page.userAdminLogs') }}
</nuxt-link>
{{ $t('version.page.userAdminLogs') }}
</v-list-item-title>
</v-list-item>
<v-list-item v-if="isReviewer && version.visibility === 'softDelete'">
<!--todo i18n & restore modal-->
<v-list-item-title>Undo delete</v-list-item-title>
</v-list-item>
<v-list-item
v-if="canHardDeleteVersion && !version.recommended && (project.info.publicVersions > 1 || version.visibility === 'softDelete')"
>
<!--todo i18n & hard delete modal-->
<v-list-item-title>Hard delete</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
<v-divider />
</v-col>
</v-row>
<v-row v-if="version">
<v-col cols="12" md="8">
<MarkdownEditor v-if="canEdit" ref="editor" :raw="version.description" :editing.sync="editingPage" :deletable="false" @save="save" />
<MarkdownEditor v-if="canEdit" ref="editor" :raw="version.description" :editing.sync="editingPage" :deletable="false" @save="savePage" />
<Markdown v-else :raw="version.description" />
</v-col>
<v-col cols="12" md="4">
@ -53,30 +67,28 @@
</template>
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator';
import { Component } from 'nuxt-property-decorator';
import { Context } from '@nuxt/types';
import { Project, Tag, Version } from 'hangar-api';
import { Prop } from 'vue-property-decorator';
import { Tag } from 'hangar-api';
import { HangarVersion } from 'hangar-internal';
import MarkdownEditor from '~/components/MarkdownEditor.vue';
import Markdown from '~/components/Markdown.vue';
import { NamedPermission, ReviewState } from '~/types/enums';
import TagComponent from '~/components/Tag.vue';
import { HangarProjectMixin } from '~/components/mixins';
// TODO implement ProjectVersionsVersionPage
@Component({
components: { TagComponent, Markdown, MarkdownEditor },
})
export default class ProjectVersionsVersionPage extends Vue {
@Prop()
project!: Project;
versions!: Version[];
export default class ProjectVersionsVersionPage extends HangarProjectMixin {
versions!: HangarVersion[];
version!: HangarVersion;
editingPage: boolean = false;
get version(): Version | null {
return this.versions && this.versions.length > 0 ? this.versions[0] : null;
}
// get version(): Version | null {
// return this.versions && this.versions.length > 0 ? this.versions[0] : null;
// }
get channel(): Tag | null {
return this.version?.tags?.find((t) => t.name === 'Channel') || null;
@ -86,7 +98,23 @@ export default class ProjectVersionsVersionPage extends Vue {
return this.$util.hasPerms(NamedPermission.EDIT_VERSION);
}
get isChecked() {
get canDeleteVersion() {
return this.$util.hasPerms(NamedPermission.DELETE_VERSION);
}
get canHardDeleteVersion() {
return this.$util.hasPerms(NamedPermission.HARD_DELETE_VERSION);
}
get canViewLogs() {
return this.$util.hasPerms(NamedPermission.VIEW_LOGS);
}
get isReviewer() {
return this.$util.hasPerms(NamedPermission.REVIEWER);
}
get isReviewStateChecked() {
return this.version?.reviewState === ReviewState.PARTIALLY_REVIEWED || this.version?.reviewState === ReviewState.REVIEWED;
}
@ -94,16 +122,40 @@ export default class ProjectVersionsVersionPage extends Vue {
return this.version?.reviewState === ReviewState.PARTIALLY_REVIEWED ? this.$t('version.page.partiallyApproved') : this.$t('version.page.approved');
}
async asyncData({ $api, $util, params }: Context) {
async asyncData({ $api, $util, params, error }: Context) {
const versions = await $api
.request<Version[]>(`projects/${params.author}/${params.slug}/versions/${params.version}`)
.catch($util.handlePageRequestError);
return { versions };
.requestInternal<HangarVersion[]>(`versions/version/${params.author}/${params.slug}/versions/${params.version}`)
.catch<any>($util.handlePageRequestError);
if (versions.length < 1) {
return error({
statusCode: 404,
});
}
// TODO maybe select default version differently?
return { versions, version: versions[0] };
}
$refs!: {
editor: MarkdownEditor;
};
savePage(content: string) {
this.$api
.requestInternal(`versions/version/${this.project.id}/${this.version.id}/saveDescription`, true, 'post', {
content,
})
.then(() => {
this.version.description = content;
this.editingPage = false;
})
.catch((err) => {
this.$refs.editor.loading.save = false;
// TODO i18n for version desc save?
this.$util.handleRequestError(err, 'page.new.error.save');
});
}
// TODO implement all of the below
save() {}
deleteVersion() {}
}
</script>

View File

@ -11,11 +11,11 @@
<v-row>
<v-col cols="12">{{ version.name }}</v-col>
<!-- todo is this order always this way? -->
<Tag :tag="version.tags[version.tags.length - 1]" />
<Tag :tag="getChannelTag(version)" />
</v-row>
</v-col>
<v-col cols="8" md="6" lg="4">
<Tag v-for="(tag, index) in version.tags.slice(0, version.tags.length - 1)" :key="index" :tag="tag" />
<Tag v-for="(tag, index) in getNonChannelTags(version)" :key="index" :tag="tag" />
</v-col>
<v-col cols="0" md="4" lg="3">
<v-row>
@ -67,7 +67,7 @@ import { Component, Prop, Vue } from 'nuxt-property-decorator';
import { PropType } from 'vue';
import { HangarProject } from 'hangar-internal';
import { Context } from '@nuxt/types';
import { PaginatedResult, Version } from 'hangar-api';
import { PaginatedResult, Tag as ApiTag, Version } from 'hangar-api';
import { NamedPermission } from '~/types/enums';
import Tag from '~/components/Tag.vue';
@ -94,6 +94,18 @@ export default class ProjectVersionsPage extends Vue {
get canUpload() {
return this.$util.hasPerms(NamedPermission.CREATE_VERSION);
}
getChannelTag(version: Version): ApiTag {
const channelTag = version.tags.find((t) => t.name === 'Channel');
if (typeof channelTag === 'undefined') {
throw new TypeError('Version missing a channel tag');
}
return channelTag;
}
getNonChannelTags(version: Version): ApiTag[] {
return version.tags.filter((t) => t.name !== 'Channel');
}
}
</script>

View File

@ -30,8 +30,10 @@ declare module 'hangar-api' {
description: string;
stats: VersionStats;
fileInfo: FileInfo;
externalUrl: string | null;
author: String;
reviewState: ReviewState;
tags: Tag[];
recommended: boolean;
}
}

View File

@ -1,5 +1,5 @@
declare module 'hangar-internal' {
import { FileInfo, Named } from 'hangar-api';
import { FileInfo, Named, Version } from 'hangar-api';
import { Platform } from '~/types/enums';
interface PlatformDependency {
@ -30,4 +30,9 @@ declare module 'hangar-internal' {
nonReviewed: boolean;
temp?: boolean;
}
interface HangarVersion extends Version {
id: number;
approvedBy?: string;
}
}

View File

@ -24,7 +24,6 @@ export function GlobalPermission(...permissions: NamedPermission[]) {
permissions,
})
.then((check) => {
console.log(check);
if (check.type !== PermissionType.GLOBAL || !check.result) {
error({
message: 'Not Found',

View File

@ -3,6 +3,7 @@ package io.papermc.hangar.controller;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.security.HangarAuthenticationToken;
import io.papermc.hangar.security.HangarPrincipal;
import io.papermc.hangar.service.internal.UserActionLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -27,6 +28,9 @@ public abstract class HangarController {
@Autowired
protected HttpServletResponse response;
@Autowired
protected UserActionLogService userActionLogService;
protected final HangarPrincipal getHangarPrincipal() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof HangarAuthenticationToken)) {

View File

@ -3,8 +3,8 @@ package io.papermc.hangar.controller.internal;
import io.papermc.hangar.controller.HangarController;
import io.papermc.hangar.model.db.projects.ProjectChannelTable;
import io.papermc.hangar.security.annotations.Anyone;
import io.papermc.hangar.security.annotations.visibility.Type;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
import io.papermc.hangar.service.internal.projects.ChannelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;

View File

@ -7,8 +7,8 @@ import io.papermc.hangar.model.internal.HangarProject;
import io.papermc.hangar.model.internal.api.requests.projects.NewProject;
import io.papermc.hangar.model.internal.api.responses.PossibleProjectOwner;
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
import io.papermc.hangar.security.annotations.visibility.Type;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
import io.papermc.hangar.service.internal.OrganizationService;
import io.papermc.hangar.service.internal.projects.ProjectFactory;
import io.papermc.hangar.service.internal.projects.ProjectService;

View File

@ -9,8 +9,8 @@ import io.papermc.hangar.model.internal.api.requests.projects.NewProjectPage;
import io.papermc.hangar.security.annotations.Anyone;
import io.papermc.hangar.security.annotations.permission.PermissionRequired;
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
import io.papermc.hangar.security.annotations.visibility.Type;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
import io.papermc.hangar.service.internal.MarkdownService;
import io.papermc.hangar.service.internal.projects.ProjectPageService;
import org.springframework.beans.factory.annotation.Autowired;
@ -54,7 +54,7 @@ public class ProjectPageController extends HangarController {
@Unlocked
@PermissionRequired(perms = NamedPermission.EDIT_PAGE, type = PermissionType.PROJECT, args = "{#projectId}")
@PostMapping("/create/{projectId}")
@PostMapping(value = "/create/{projectId}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
public ResponseEntity<String> createProjectPage(@PathVariable long projectId, @RequestBody @Valid NewProjectPage newProjectPage) {
return ResponseEntity.ok(projectPageService.createProjectPage(projectId, newProjectPage));
@ -62,7 +62,7 @@ public class ProjectPageController extends HangarController {
@Unlocked
@PermissionRequired(perms = NamedPermission.EDIT_PAGE, type = PermissionType.PROJECT, args = "{#projectId}")
@PostMapping("/save/{projectId}/{pageId}")
@PostMapping(value = "/save/{projectId}/{pageId}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
public void saveProjectPage(@PathVariable long projectId, @PathVariable long pageId, @RequestBody @Valid StringContent content) {
projectPageService.saveProjectPage(projectId, pageId, content.getContent());

View File

@ -1,18 +1,28 @@
package io.papermc.hangar.controller.internal;
import io.papermc.hangar.controller.HangarController;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.customtypes.LoggedActionType.VersionContext;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.PermissionType;
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.internal.api.requests.StringContent;
import io.papermc.hangar.model.internal.versions.HangarVersion;
import io.papermc.hangar.model.internal.versions.PendingVersion;
import io.papermc.hangar.security.annotations.permission.PermissionRequired;
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
import io.papermc.hangar.service.internal.versions.VersionFactory;
import io.papermc.hangar.service.internal.versions.VersionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -22,6 +32,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.util.List;
@Controller
@Secured("ROLE_USER")
@ -29,10 +40,18 @@ import javax.validation.Valid;
public class VersionController extends HangarController {
private final VersionFactory versionFactory;
private final VersionService versionService;
@Autowired
public VersionController(VersionFactory versionFactory) {
public VersionController(VersionFactory versionFactory, VersionService versionService) {
this.versionFactory = versionFactory;
this.versionService = versionService;
}
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #slug}")
@GetMapping(path = "/version/{author}/{slug}/versions/{versionString}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<HangarVersion>> getVersions(@PathVariable String author, @PathVariable String slug, @PathVariable String versionString) {
return ResponseEntity.ok(versionService.getHangarVersions(author, slug, versionString));
}
@Unlocked
@ -59,4 +78,20 @@ public class VersionController extends HangarController {
System.out.println(pendingVersion);
versionFactory.publishPendingVersion(projectId, pendingVersion);
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.EDIT_VERSION, args = "{#projectId}")
@PostMapping(path = "/version/{projectId}/{versionId}/saveDescription", consumes = MediaType.APPLICATION_JSON_VALUE)
public void saveDescription(@PathVariable long projectId, @PathVariable long versionId, @Valid @RequestBody StringContent stringContent) {
ProjectVersionTable projectVersionTable = versionService.getProjectVersionTable(versionId);
if (projectVersionTable == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
String oldDesc = projectVersionTable.getDescription();
String newDesc = stringContent.getContent().trim();
projectVersionTable.setDescription(newDesc);
versionService.updateProjectVersionTable(projectVersionTable);
userActionLogService.version(LoggedActionType.VERSION_DESCRIPTION_CHANGED.with(VersionContext.of(projectId, versionId)), newDesc, oldDesc);
}
}

View File

@ -0,0 +1,80 @@
package io.papermc.hangar.db.dao.internal;
import io.papermc.hangar.db.dao.v1.VersionsApiDAO.VersionReducer;
import io.papermc.hangar.model.api.project.version.PluginDependency;
import io.papermc.hangar.model.internal.versions.HangarVersion;
import org.jdbi.v3.core.enums.EnumStrategy;
import org.jdbi.v3.core.result.LinkedHashMapRowReducer;
import org.jdbi.v3.core.result.RowView;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.config.UseEnumStrategy;
import org.jdbi.v3.sqlobject.customizer.Define;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.UseRowReducer;
import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
@RegisterConstructorMapper(HangarVersion.class)
public interface HangarVersionsDAO {
@UseEnumStrategy(EnumStrategy.BY_ORDINAL)
@UseRowReducer(HangarVersionReducer.class)
@RegisterConstructorMapper(value = PluginDependency.class, prefix = "pd_")
@UseStringTemplateEngine
@SqlQuery("SELECT pv.id," +
" pv.created_at," +
" pv.version_string," +
" pv.visibility," +
" pv.description," +
" coalesce((SELECT sum(pvd.downloads) FROM project_versions_downloads pvd WHERE p.id = pvd.project_id AND pv.id = pvd.version_id), 0) vs_downloads," +
" pv.file_name fi_name," +
" pv.file_size fi_size_bytes," +
" pv.hash fi_md5_hash," +
" pv.external_url," +
" u.name author," +
" pv.review_state," +
" pvt.name AS tag_name," +
" pvt.data AS tag_data," +
" pvt.color AS tag_color," +
" 'Channel' AS ch_tag_name," +
" pc.name AS ch_tag_data," +
" pc.color AS ch_tag_color," +
" d.platform pd_platform," +
" d.name pd_name," +
" d.required pd_required," +
" d.project_id pd_project_id," +
" d.external_url pd_external_url," +
" plv.platform p_platform," +
" plv.version p_version," +
" exists(SELECT 1 FROM recommended_project_versions rpv WHERE rpv.version_id = pv.id) as recommended," +
" ru.name approved_by" +
" FROM project_versions pv" +
" JOIN projects p ON pv.project_id = p.id" +
" LEFT JOIN users u ON pv.author_id = u.id" +
" LEFT JOIN project_version_tags pvt ON pv.id = pvt.version_id" +
" LEFT JOIN project_channels pc ON pv.channel_id = pc.id " +
" JOIN project_version_platform_dependencies pvpd ON pv.id = pvpd.version_id" +
" JOIN platform_versions plv ON pvpd.platform_version_id = plv.id" +
" LEFT JOIN project_version_dependencies d ON pv.id = d.version_id" +
" LEFT JOIN users ru ON pv.reviewer_id = ru.id" +
" WHERE <if(!canSeeHidden)>(pv.visibility = 0 " +
" <if(userId)>OR (<userId> IN (SELECT pm.user_id FROM project_members_all pm WHERE pm.id = p.id) AND pv.visibility != 4) <endif>) AND <endif> " +
" pvt.name IS NOT NULL AND" +
" lower(p.owner_name) = lower(:author) AND" +
" lower(p.slug) = lower(:slug) AND" +
" lower(pv.version_string) = lower(:versionString)" +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id, ru.id" +
" ORDER BY pv.created_at DESC")
List<HangarVersion> getVersions(String author, String slug, String versionString, @Define boolean canSeeHidden, @Define Long userId);
class HangarVersionReducer implements LinkedHashMapRowReducer<Long, HangarVersion> {
@Override
public void accumulate(Map<Long, HangarVersion> container, RowView rowView) {
final HangarVersion version = container.computeIfAbsent(rowView.getColumn("id", Long.class), id -> rowView.getRow(HangarVersion.class));
VersionReducer._accumulateVersion(rowView, version.getPluginDependencies(), version.getPlatformDependencies(), version.getTags(), version);
}
}
}

View File

@ -35,6 +35,9 @@ public interface ProjectVersionsDAO {
@SqlQuery("SELECT * FROM project_versions WHERE project_id = :projectId AND hash = :hash AND version_string = :versionString")
ProjectVersionTable getProjectVersionTable(long projectId, String hash, String versionString);
@SqlQuery("SELECT * FROM project_versions pv WHERE pv.id = :versionId")
ProjectVersionTable getProjectVersionTable(long versionId);
@Timestamped
@SqlBatch("INSERT INTO project_version_tags (created_at, version_id, name, data, color) VALUES (:now, :versionId, :name, :data, :color)")
void insertTags(@BindBean Collection<ProjectVersionTagTable> projectVersionTagTables);

View File

@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Repository
@UseStringTemplateEngine
@ -45,6 +46,7 @@ public interface VersionsApiDAO {
" pv.file_name fi_name," +
" pv.file_size fi_size_bytes," +
" pv.hash fi_md5_hash," +
" pv.external_url," +
" u.name author," +
" pv.review_state," +
" pvt.name AS tag_name," +
@ -59,7 +61,9 @@ public interface VersionsApiDAO {
" d.project_id pd_project_id," +
" d.external_url pd_external_url," +
" plv.platform p_platform," +
" plv.version p_version" +
" plv.version p_version," +
" exists(SELECT 1 FROM recommended_project_versions rpv WHERE rpv.version_id = pv.id) as recommended," +
" ru.name approved_by" +
" FROM project_versions pv" +
" JOIN projects p ON pv.project_id = p.id" +
" LEFT JOIN users u ON pv.author_id = u.id" +
@ -68,6 +72,7 @@ public interface VersionsApiDAO {
" JOIN project_version_platform_dependencies pvpd ON pv.id = pvpd.version_id" +
" JOIN platform_versions plv ON pvpd.platform_version_id = plv.id" +
" LEFT JOIN project_version_dependencies d ON pv.id = d.version_id" +
" LEFT JOIN users ru ON pv.reviewer_id = ru.id" +
" WHERE <if(!canSeeHidden)>(pv.visibility = 0 " +
" <if(userId)>OR (<userId> IN (SELECT pm.user_id FROM project_members_all pm WHERE pm.id = p.id) AND pv.visibility != 4) <endif>) AND <endif> " +
" plv.platform = :platform AND" +
@ -75,7 +80,7 @@ public interface VersionsApiDAO {
" lower(p.owner_name) = lower(:author) AND" +
" lower(p.slug) = lower(:slug) AND" +
" lower(pv.version_string) = lower(:versionString)" +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id " +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id, ru.id " +
" ORDER BY pv.created_at DESC")
Version getVersion(String author, String slug, String versionString, @EnumByOrdinal Platform platform, @Define boolean canSeeHidden, @Define Long userId);
@ -91,6 +96,7 @@ public interface VersionsApiDAO {
" pv.file_name fi_name," +
" pv.file_size fi_size_bytes," +
" pv.hash fi_md5_hash," +
" pv.external_url," +
" u.name author," +
" pv.review_state," +
" pvt.name AS tag_name," +
@ -105,7 +111,9 @@ public interface VersionsApiDAO {
" d.project_id pd_project_id," +
" d.external_url pd_external_url," +
" plv.platform p_platform," +
" plv.version p_version" +
" plv.version p_version," +
" exists(SELECT 1 FROM recommended_project_versions rpv WHERE rpv.version_id = pv.id) as recommended," +
" ru.name approved_by" +
" FROM project_versions pv" +
" JOIN projects p ON pv.project_id = p.id" +
" LEFT JOIN users u ON pv.author_id = u.id" +
@ -114,37 +122,41 @@ public interface VersionsApiDAO {
" JOIN project_version_platform_dependencies pvpd ON pv.id = pvpd.version_id" +
" JOIN platform_versions plv ON pvpd.platform_version_id = plv.id" +
" LEFT JOIN project_version_dependencies d ON pv.id = d.version_id" +
" LEFT JOIN users ru ON pv.reviewer_id = ru.id" +
" WHERE <if(!canSeeHidden)>(pv.visibility = 0 " +
" <if(userId)>OR (<userId> IN (SELECT pm.user_id FROM project_members_all pm WHERE pm.id = p.id) AND pv.visibility != 4) <endif>) AND <endif> " +
" pvt.name IS NOT NULL AND" +
" lower(p.owner_name) = lower(:author) AND" +
" lower(p.slug) = lower(:slug) AND" +
" lower(pv.version_string) = lower(:versionString)" +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id " +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id, ru.id" +
" ORDER BY pv.created_at DESC")
List<Version> getVersions(String author, String slug, String versionString, @Define boolean canSeeHidden, @Define Long userId);
class VersionReducer implements LinkedHashMapRowReducer<Long, Version> { // What a mess really
class VersionReducer implements LinkedHashMapRowReducer<Long, Version> {
@Override
public void accumulate(Map<Long, Version> container, RowView rowView) {
final Version version = container.computeIfAbsent(rowView.getColumn("id", Long.class), id -> rowView.getRow(Version.class));
VersionReducer._accumulateVersion(rowView, version.getPluginDependencies(), version.getPlatformDependencies(), version.getTags(), version);
}
public static <T extends Version> void _accumulateVersion(RowView rowView, Map<Platform, Set<PluginDependency>> pluginDependencies, Map<Platform, Set<String>> platformDependencies, Set<Tag> tags, T version) { // What a mess really
Platform pluginPlatform = rowView.getColumn("pd_platform", Platform.class);
if (pluginPlatform != null) {
version.getPluginDependencies().computeIfAbsent(pluginPlatform, _pl -> new HashSet<>());
version.getPluginDependencies().get(pluginPlatform).add(rowView.getRow(PluginDependency.class));
pluginDependencies.computeIfAbsent(pluginPlatform, _pl -> new HashSet<>());
pluginDependencies.get(pluginPlatform).add(rowView.getRow(PluginDependency.class));
}
Platform platformPlatform = rowView.getColumn("p_platform", Platform.class);
version.getPlatformDependencies().computeIfAbsent(platformPlatform, _pl -> new HashSet<>());
version.getPlatformDependencies().get(platformPlatform).add(rowView.getColumn("p_version", String.class));
platformDependencies.computeIfAbsent(platformPlatform, _pl -> new HashSet<>());
platformDependencies.get(platformPlatform).add(rowView.getColumn("p_version", String.class));
if (rowView.getColumn("ch_tag_name", String.class) != null) {
version.getTags().add(new Tag(rowView.getColumn("ch_tag_name", String.class), rowView.getColumn("ch_tag_data", String.class), new TagColor(null, rowView.getColumn("ch_tag_color", Color.class).getHex())));
tags.add(new Tag(rowView.getColumn("ch_tag_name", String.class), rowView.getColumn("ch_tag_data", String.class), new TagColor(null, rowView.getColumn("ch_tag_color", Color.class).getHex())));
}
if (rowView.getColumn("tag_name", String.class) != null) {
version.getTags().add(new Tag(
tags.add(new Tag(
rowView.getColumn("tag_name", String.class),
StringUtils.formatVersionNumbers(Arrays.asList(rowView.getColumn("tag_data", String[].class))),
rowView.getColumn("tag_color", io.papermc.hangar.model.common.TagColor.class).toTagColor()
@ -165,6 +177,7 @@ public interface VersionsApiDAO {
" pv.file_name fi_name," +
" pv.file_size fi_size_bytes," +
" pv.hash fi_md5_hash," +
" pv.external_url," +
" u.name author," +
" pv.review_state," +
" pvt.name AS tag_name," +
@ -179,7 +192,9 @@ public interface VersionsApiDAO {
" d.project_id pd_project_id," +
" d.external_url pd_external_url," +
" plv.platform p_platform," +
" plv.version p_version" +
" plv.version p_version," +
" exists(SELECT 1 FROM recommended_project_versions rpv WHERE rpv.version_id = pv.id) as recommended," +
" ru.name approved_by" +
" FROM project_versions pv" +
" JOIN projects p ON pv.project_id = p.id" +
" LEFT JOIN users u ON pv.author_id = u.id" +
@ -188,12 +203,13 @@ public interface VersionsApiDAO {
" JOIN project_version_platform_dependencies pvpd ON pv.id = pvpd.version_id" +
" JOIN platform_versions plv ON pvpd.platform_version_id = plv.id" +
" LEFT JOIN project_version_dependencies d ON pv.id = d.version_id" +
" LEFT JOIN users ru ON pv.reviewer_id = ru.id" +
" WHERE <if(!canSeeHidden)>(pv.visibility = 0 " +
" <if(userId)>OR (<userId> IN (SELECT pm.user_id FROM project_members_all pm WHERE pm.id = p.id) AND pv.visibility != 4) <endif>) AND <endif> " +
" pvt.name IS NOT NULL AND" +
" lower(p.owner_name) = lower(:author) AND" +
" lower(p.slug) = lower(:slug)" +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id " +
" GROUP BY p.id, pv.id, u.id, pc.id, d.id, plv.id, pvt.id, ru.id " +
" ORDER BY pv.created_at DESC LIMIT :limit OFFSET :offset")
List<Version> getVersions(String author, String slug, @BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List<String> tags, @Define boolean canSeeHidden, @Define Long userId, long limit, long offset);

View File

@ -25,13 +25,17 @@ public class Version extends Model implements Named, Visible {
private final String description;
private final VersionStats stats;
private final FileInfo fileInfo;
private final String externalUrl;
private final String author;
private final ReviewState reviewState;
private final Set<Tag> tags;
private final boolean recommended;
public Version(OffsetDateTime createdAt, @ColumnName("version_string") String name, Visibility visibility, String description, @Nested("vs") VersionStats stats, @Nested("fi") FileInfo fileInfo, String author, @EnumByOrdinal ReviewState reviewState) {
public Version(OffsetDateTime createdAt, @ColumnName("version_string") String name, Visibility visibility, String description, @Nested("vs") VersionStats stats, @Nested("fi") FileInfo fileInfo, String externalUrl, String author, @EnumByOrdinal ReviewState reviewState, boolean recommended) {
super(createdAt);
this.name = name;
this.externalUrl = externalUrl;
this.recommended = recommended;
this.tags = new HashSet<>();
this.pluginDependencies = new EnumMap<>(Platform.class);
this.platformDependencies = new EnumMap<>(Platform.class);
@ -73,6 +77,10 @@ public class Version extends Model implements Named, Visible {
return fileInfo;
}
public String getExternalUrl() {
return externalUrl;
}
public String getAuthor() {
return author;
}
@ -85,19 +93,25 @@ public class Version extends Model implements Named, Visible {
return tags;
}
public boolean isRecommended() {
return recommended;
}
@Override
public String toString() {
return "Version{" +
"versionString='" + name + '\'' +
"name='" + name + '\'' +
", pluginDependencies=" + pluginDependencies +
", platformDependencies=" + platformDependencies +
", visibility=" + visibility +
", description='" + description + '\'' +
", stats=" + stats +
", fileInfo=" + fileInfo +
", externalUrl='" + externalUrl + '\'' +
", author='" + author + '\'' +
", reviewState=" + reviewState +
", tags=" + tags +
", recommended=" + recommended +
"} " + super.toString();
}
}

View File

@ -120,6 +120,7 @@ public class ProjectVersionTable extends Table implements Named, ModelVisible, P
}
@Override
@EnumByOrdinal
public Visibility getVisibility() {
return visibility;
}
@ -129,6 +130,7 @@ public class ProjectVersionTable extends Table implements Named, ModelVisible, P
this.visibility = visibility;
}
@EnumByOrdinal
public ReviewState getReviewState() {
return reviewState;
}

View File

@ -76,6 +76,7 @@ public class HangarProject extends Project implements Joinable<ProjectRoleTable>
", lastVisibilityChangeComment='" + lastVisibilityChangeComment + '\'' +
", lastVisibilityChangeUserName='" + lastVisibilityChangeUserName + '\'' +
", info=" + info +
", pages=" + pages +
"} " + super.toString();
}

View File

@ -0,0 +1,45 @@
package io.papermc.hangar.model.internal.versions;
import io.papermc.hangar.config.jackson.RequiresPermission;
import io.papermc.hangar.model.Identified;
import io.papermc.hangar.model.api.project.version.FileInfo;
import io.papermc.hangar.model.api.project.version.Version;
import io.papermc.hangar.model.api.project.version.VersionStats;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.projects.ReviewState;
import io.papermc.hangar.model.common.projects.Visibility;
import org.jdbi.v3.core.enums.EnumByOrdinal;
import org.jdbi.v3.core.mapper.Nested;
import org.jdbi.v3.core.mapper.reflect.ColumnName;
import java.time.OffsetDateTime;
public class HangarVersion extends Version implements Identified {
private final long id;
private final String approvedBy;
public HangarVersion(OffsetDateTime createdAt, @ColumnName("version_string") String name, Visibility visibility, String description, @Nested("vs") VersionStats stats, @Nested("fi") FileInfo fileInfo, String externalUrl, String author, @EnumByOrdinal ReviewState reviewState, boolean recommended, long id, String approvedBy) {
super(createdAt, name, visibility, description, stats, fileInfo, externalUrl, author, reviewState, recommended);
this.id = id;
this.approvedBy = approvedBy;
}
@Override
public long getId() {
return id;
}
@RequiresPermission(NamedPermission.REVIEWER)
public String getApprovedBy() {
return approvedBy;
}
@Override
public String toString() {
return "HangarVersion{" +
"id=" + id +
", approvedBy='" + approvedBy + '\'' +
"} " + super.toString();
}
}

View File

@ -1,18 +0,0 @@
package io.papermc.hangar.security.annotations.visibility;
import java.util.Set;
public enum Type {
PROJECT(1, 2),
VERSION(1, 3);
private final Set<Integer> argCount;
Type(Integer...argCounts) {
this.argCount = Set.of(argCounts);
}
public Set<Integer> getArgCount() {
return argCount;
}
}

View File

@ -5,6 +5,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Set;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ -13,4 +14,19 @@ public @interface VisibilityRequired {
Type type();
String args();
enum Type {
PROJECT(1, 2),
VERSION(1, 3);
private final Set<Integer> argCount;
Type(Integer...argCounts) {
this.argCount = Set.of(argCounts);
}
public Set<Integer> getArgCount() {
return argCount;
}
}
}

View File

@ -22,15 +22,15 @@ public class VisibilityRequiredMetadataExtractor implements AnnotationMetadataEx
static class VisibilityRequiredAttribute implements ConfigAttribute {
private final Type type;
private final VisibilityRequired.Type type;
private final Expression expression;
VisibilityRequiredAttribute(Type type, Expression expression) {
VisibilityRequiredAttribute(VisibilityRequired.Type type, Expression expression) {
this.type = type;
this.expression = expression;
}
public Type getType() {
public VisibilityRequired.Type getType() {
return type;
}

View File

@ -30,16 +30,20 @@ public class VersionsApiService extends HangarService {
}
public Version getVersion(String author, String slug, String versionString, Platform platform) {
return versionsApiDAO.getVersion(author, slug, versionString, platform, getHangarPrincipal().getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
return versionsApiDAO.getVersion(author, slug, versionString, platform, getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
}
public List<Version> getVersions(String author, String slug, String versionString) {
return versionsApiDAO.getVersions(author, slug, versionString, getHangarPrincipal().getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
List<Version> versions = versionsApiDAO.getVersions(author, slug, versionString, getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
if (versions.isEmpty()) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
return versions;
}
public PaginatedResult<Version> getVersions(String author, String slug, List<String> tags, RequestPagination pagination) {
List<Version> versions = versionsApiDAO.getVersions(author, slug, tags, getHangarPrincipal().getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId(), pagination.getLimit(), pagination.getOffset());
Long versionCount = versionsApiDAO.getVersionCount(author, slug, tags, getHangarPrincipal().getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
List<Version> versions = versionsApiDAO.getVersions(author, slug, tags, getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId(), pagination.getLimit(), pagination.getOffset());
Long versionCount = versionsApiDAO.getVersionCount(author, slug, tags, getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
return new PaginatedResult<>(new Pagination(versionCount == null ? 0 : versionCount, pagination), versions);
}

View File

@ -1,23 +1,56 @@
package io.papermc.hangar.service.internal.versions;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.HangarVersionsDAO;
import io.papermc.hangar.db.dao.internal.table.versions.ProjectVersionsDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.common.TagColor;
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.db.versions.ProjectVersionTagTable;
import io.papermc.hangar.model.internal.versions.HangarVersion;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.VisibilityService.ProjectVersionVisibilityService;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
@Service
public class VersionService extends HangarService {
private final ProjectVersionsDAO projectVersionsDAO;
private final HangarVersionsDAO hangarVersionsDAO;
private final ProjectVersionVisibilityService projectVersionVisibilityService;
@Autowired
public VersionService(HangarDao<ProjectVersionsDAO> projectVersionDAO) {
public VersionService(HangarDao<ProjectVersionsDAO> projectVersionDAO, HangarDao<HangarVersionsDAO> hangarProjectsDAO, ProjectVersionVisibilityService projectVersionVisibilityService) {
this.projectVersionsDAO = projectVersionDAO.get();
this.hangarVersionsDAO = hangarProjectsDAO.get();
this.projectVersionVisibilityService = projectVersionVisibilityService;
}
@Nullable
public ProjectVersionTable getProjectVersionTable(Long versionId) {
if (versionId == null) {
return null;
}
return projectVersionVisibilityService.checkVisibility(projectVersionsDAO.getProjectVersionTable(versionId));
}
public void updateProjectVersionTable(ProjectVersionTable projectVersionTable) {
projectVersionsDAO.update(projectVersionTable);
}
public List<HangarVersion> getHangarVersions(String author, String slug, String versionString) {
List<HangarVersion> versions = hangarVersionsDAO.getVersions(author, slug, versionString, getGlobalPermissions().has(Permission.SeeHidden), getHangarUserId());
if (versions.isEmpty()) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
return versions;
}
public void addUnstableTag(long versionId) {

View File

@ -252,7 +252,9 @@ CREATE TABLE project_version_platform_dependencies
CONSTRAINT project_version_platform_dependencies_platform_version_id_fkey
FOREIGN KEY (platform_version_id)
REFERENCES platform_versions
ON DELETE CASCADE
ON DELETE CASCADE,
CONSTRAINT project_version_platform_dependencies_unique
UNIQUE (version_id, platform_version_id)
);
CREATE TABLE roles