mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-31 16:00:39 +08:00
cleanup and finished project pages
This commit is contained in:
parent
9b8e45ca08
commit
fb1efe97e5
@ -37,7 +37,6 @@ hangar:
|
||||
auth-url: "http://localhost:8000"
|
||||
base-url: "http://localhost:8080"
|
||||
plugin-upload-dir: "/uploads"
|
||||
use-webpack: true
|
||||
|
||||
sponsors:
|
||||
- name: Beer
|
||||
@ -53,7 +52,7 @@ hangar:
|
||||
channels:
|
||||
max-name-len: 15
|
||||
name-regex: "^[a-zA-Z0-9]+$"
|
||||
color-default: 7
|
||||
color-default: cyan
|
||||
name-default: "Release"
|
||||
|
||||
pages:
|
||||
@ -61,9 +60,10 @@ hangar:
|
||||
name: "Home"
|
||||
message: "Welcome to your new project!"
|
||||
min-len: 15
|
||||
max-len: 32000
|
||||
page:
|
||||
max-len: 75000
|
||||
max-len: 75000
|
||||
name-regex: "^[a-zA-Z0-9-_ ]+$"
|
||||
max-name-len: 25
|
||||
min-name-len: 3
|
||||
|
||||
projects:
|
||||
max-name-len: 25
|
||||
@ -81,6 +81,11 @@ hangar:
|
||||
unsafe-download-max-age: 10
|
||||
name-regex: "^[a-zA-Z0-9-_]{3,}$"
|
||||
|
||||
orgs:
|
||||
create-limit: 5
|
||||
dummy-email-domain: "org.papermc.io"
|
||||
enabled: true
|
||||
|
||||
users:
|
||||
max-tagline-len: 100
|
||||
author-page-size: 25
|
||||
@ -115,7 +120,5 @@ hangar:
|
||||
avatar-url: "http://localhost:8000/avatar/%s?size=120x120" # only comment in if you run auth locally
|
||||
key: changeme
|
||||
timeout: 10000
|
||||
breaker:
|
||||
max-failures: 5
|
||||
timeout: 10s
|
||||
reset: 5m
|
||||
safe-download-hosts:
|
||||
- "github.com"
|
||||
|
@ -15,10 +15,11 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-menu v-if="$util.isLoggedIn()" bottom offset-y transition="slide-y-transition">
|
||||
<v-menu v-if="$util.isLoggedIn()" bottom offset-y transition="slide-y-transition" open-on-hover>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" color="primary" v-on="on">
|
||||
{{ $t('nav.createNew') }}
|
||||
<v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<Dropdown :controls="newControls" />
|
||||
|
@ -8,7 +8,7 @@
|
||||
<v-card-text>
|
||||
<v-form ref="modalForm" v-model="validForm">
|
||||
<v-text-field v-model.trim="form.name" filled :rules="[$util.$vc.require('Page name')]" :label="$t('page.new.name')" />
|
||||
<v-select v-model="form.parent" :items="pages" filled clearable :label="$t('page.new.parent')" item-text="name" item-value="id" />
|
||||
<v-select v-model="form.parent" :items="pageRoots" filled clearable :label="$t('page.new.parent')" item-text="name" item-value="id" />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
@ -38,6 +38,21 @@ export default class NewPageModal extends HangarFormModal {
|
||||
@Prop({ type: Array as PropType<HangarProjectPage[]>, required: true })
|
||||
pages!: HangarProjectPage[];
|
||||
|
||||
get pageRoots(): HangarProjectPage[] {
|
||||
return this.flatDeep(this.pages);
|
||||
}
|
||||
|
||||
flatDeep(pages: HangarProjectPage[]): HangarProjectPage[] {
|
||||
let ps: HangarProjectPage[] = [];
|
||||
for (const page of pages) {
|
||||
if (page.children.length > 0) {
|
||||
ps = ps.concat(this.flatDeep(page.children));
|
||||
}
|
||||
ps.push(page);
|
||||
}
|
||||
return ps;
|
||||
}
|
||||
|
||||
createPage() {
|
||||
this.loading = true;
|
||||
this.$api
|
||||
@ -47,7 +62,7 @@ export default class NewPageModal extends HangarFormModal {
|
||||
})
|
||||
.then((slug) => {
|
||||
this.dialog = false;
|
||||
this.$nuxt.refresh(); // Might be needed to refresh the HangarProject w/the new page? idk - jake
|
||||
this.$nuxt.refresh();
|
||||
this.$router.push(`/${this.$route.params.author}/${this.$route.params.slug}/pages/${slug}`);
|
||||
})
|
||||
.catch(this.$util.handleRequestError)
|
||||
|
@ -155,8 +155,12 @@ const msgs: LocaleMessageObject = {
|
||||
title: 'Create a new page',
|
||||
error: {
|
||||
minLength: 'Page name was too short',
|
||||
invalidName: 'Page names must be between 3 and 25 alphanumeric (+ underscore & hyphen) characters',
|
||||
duplicateName: 'A page with that name already exists',
|
||||
name: {
|
||||
maxLength: 'Page name too long',
|
||||
minLength: 'Page name too short',
|
||||
invalidChars: 'Page name contained invalid characters',
|
||||
},
|
||||
},
|
||||
name: 'Page Name',
|
||||
parent: 'Parent Page (optional)',
|
||||
|
@ -145,7 +145,6 @@ export default class ProjectPage extends Vue {
|
||||
}
|
||||
|
||||
async asyncData({ $api, params, $util }: Context) {
|
||||
console.log('asyncData ProjectPage');
|
||||
const project = await $api
|
||||
.requestInternal<HangarProject>(`projects/project/${params.author}/${params.slug}`, false)
|
||||
.catch($util.handlePageRequestError);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col v-if="page.contents" cols="12" md="8">
|
||||
<v-col v-if="page.contents" cols="12" md="9">
|
||||
<MarkdownEditor
|
||||
v-if="canEdit"
|
||||
ref="editor"
|
||||
@ -13,7 +13,7 @@
|
||||
/>
|
||||
<Markdown v-else :raw="page.contents" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col cols="12" md="3">
|
||||
<ProjectPageList :project="project" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -37,12 +37,7 @@ import ProjectPageList from '~/components/projects/ProjectPageList.vue';
|
||||
},
|
||||
})
|
||||
export default class VueProjectPage extends DocPageMixin {
|
||||
async asyncData({ $api, params, $util, beforeNuxtRender }: Context) {
|
||||
if (process.server) {
|
||||
beforeNuxtRender(({ nuxtState }) => {
|
||||
console.log(nuxtState);
|
||||
});
|
||||
}
|
||||
async asyncData({ $api, params, $util }: Context) {
|
||||
const page = await $api
|
||||
.requestInternal<ProjectPage>(`pages/page/${params.author}/${params.slug}/${params.pathMatch}`, false)
|
||||
.catch<any>($util.handlePageRequestError);
|
||||
|
@ -234,7 +234,6 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'nuxt-property-decorator';
|
||||
import { Context } from '@nuxt/types';
|
||||
import { HangarValidationException } from 'hangar-api';
|
||||
import { ProjectOwner } from 'hangar-internal';
|
||||
import StepperStepContent from '~/components/steppers/StepperStepContent.vue';
|
||||
import { RootState } from '~/store';
|
||||
@ -326,12 +325,7 @@ export default class NewPage extends Vue {
|
||||
})
|
||||
.catch((err) => {
|
||||
this.projectError = true;
|
||||
if (err.response?.data?.isHangarValidationException) {
|
||||
const validationError: HangarValidationException = err.response.data;
|
||||
for (const fieldError of validationError.fieldErrors) {
|
||||
this.$util.error(fieldError.errorMsg);
|
||||
}
|
||||
}
|
||||
this.$util.handleRequestError(err, 'Unable to create project');
|
||||
})
|
||||
.finally(() => {
|
||||
this.projectLoading = false;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Context } from '@nuxt/types';
|
||||
import { Inject } from '@nuxt/types/app';
|
||||
import { AxiosError } from 'axios';
|
||||
import { HangarException } from 'hangar-api';
|
||||
import { HangarApiException, HangarValidationException } from 'hangar-api';
|
||||
import { HangarUser } from 'hangar-internal';
|
||||
import { NamedPermission } from '~/types/enums';
|
||||
import { RootState } from '~/store';
|
||||
@ -19,12 +19,18 @@ function handleRequestError(err: AxiosError, error: Context['error']) {
|
||||
});
|
||||
console.log(err);
|
||||
} else if (err.response) {
|
||||
if (err.response.data.isHangarApiException || err.response.data.isHangarValidationException) {
|
||||
const data: HangarException = err.response.data;
|
||||
if (err.response.data.isHangarApiException) {
|
||||
const data: HangarApiException = err.response.data;
|
||||
error({
|
||||
statusCode: data.httpError.statusCode,
|
||||
message: data.message,
|
||||
});
|
||||
} else if (err.response.data.isHangarValidationException) {
|
||||
const data: HangarValidationException = err.response.data;
|
||||
error({
|
||||
statusCode: data.httpError.statusCode,
|
||||
message: data.fieldErrors.map((f) => f.errorMsg).join(', '),
|
||||
});
|
||||
} else {
|
||||
error({
|
||||
statusCode: err.response.status,
|
||||
@ -132,13 +138,29 @@ const createUtil = ({ store, error }: Context) => {
|
||||
console.log(err);
|
||||
} else if (err.response) {
|
||||
// TODO check is msg is a i18n key and use that instead
|
||||
if (err.response.data.isHangarValidationException || err.response.data.isHangarApiException) {
|
||||
const data: HangarException = err.response.data;
|
||||
if (err.response.data.isHangarApiException) {
|
||||
const data: HangarApiException = err.response.data;
|
||||
store.dispatch('snackbar/SHOW_NOTIF', {
|
||||
message: msg ? `${msg}: ${data.message}` : data.message,
|
||||
color: 'error',
|
||||
timeout: 3000,
|
||||
} as NotifPayload);
|
||||
} else if (err.response.data.isHangarValidationException) {
|
||||
const data: HangarValidationException = err.response.data;
|
||||
for (const fieldError of data.fieldErrors) {
|
||||
store.dispatch('snackbar/SHOW_NOTIF', {
|
||||
message: fieldError.errorMsg,
|
||||
color: 'error',
|
||||
timeout: 3000,
|
||||
} as NotifPayload);
|
||||
}
|
||||
if (msg) {
|
||||
store.dispatch('snackbar/SHOW_NOTIF', {
|
||||
message: msg,
|
||||
color: 'error',
|
||||
timeout: 3000,
|
||||
} as NotifPayload);
|
||||
}
|
||||
} else {
|
||||
store.dispatch('snackbar/SHOW_NOTIF', {
|
||||
message: msg ? `${msg}: ${err.response.statusText}` : err.response.statusText,
|
||||
|
@ -4,7 +4,7 @@
|
||||
[#-- @ftlvariable name="mapper" type="com.fasterxml.jackson.databind.ObjectMapper" --]
|
||||
[#-- @ftlvariable name="_csrf" type="org.springframework.security.web.csrf.CsrfToken" --]
|
||||
[#-- @ftlvariable name="cu" type="io.papermc.hangar.db.modelold.UsersTable" --]
|
||||
[#-- @ftlvariable name="markdownService" type="io.papermc.hangar.serviceold.MarkdownService" --]
|
||||
[#-- @ftlvariable name="markdownService" type="io.papermc.hangar.service.internal.MarkdownService" --]
|
||||
[#-- @ftlvariable name="utils" type="io.papermc.hangar.util.TemplateHelper" --]
|
||||
[#-- @ftlvariable name="@helper" type="freemarker.template.TemplateHashModel" --]
|
||||
[#-- @ftlvariable name="alerts" type="java.util.Map" --]
|
||||
|
@ -9,6 +9,7 @@ public class CacheConfig {
|
||||
|
||||
public static final String AUTHORS_CACHE = "AUTHORS_CACHE";
|
||||
public static final String STAFF_CACHE = "STAFF_CACHE";
|
||||
// TODO dont need these caches anymore
|
||||
public static final String PENDING_VERSION_CACHE = "PENDING_VERSION_CACHE";
|
||||
public static final String NEW_VERSION_CACHE = "NEW_VERSION_CACHE";
|
||||
|
||||
|
@ -8,6 +8,7 @@ import org.springframework.context.support.ReloadableResourceBundleMessageSource
|
||||
@Configuration
|
||||
public class UtilsConfig {
|
||||
|
||||
// TODO remove cause its now client side stuff
|
||||
@Bean
|
||||
public MessageSource messageSource() {
|
||||
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
|
@ -1,17 +1,19 @@
|
||||
package io.papermc.hangar.config.hangar;
|
||||
|
||||
import io.papermc.hangar.model.common.Color;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.channels")
|
||||
public class ChannelsConfig {
|
||||
@Size(min = 1)
|
||||
private int maxNameLen = 15;
|
||||
private String nameRegex = "^[a-zA-Z0-9]+$";
|
||||
@Value("#{T(io.papermc.hangar.model.common.Color).getById(${hangar.channels.color-default})}")
|
||||
private Color colorDefault = Color.getById(7);
|
||||
private Color colorDefault = Color.CYAN;
|
||||
@Size(min = 1, max = 15)
|
||||
private String nameDefault = "Release";
|
||||
|
||||
public int getMaxNameLen() {
|
||||
|
@ -44,7 +44,7 @@ public class HangarConfig {
|
||||
@NestedConfigurationProperty
|
||||
public UserConfig user;
|
||||
@NestedConfigurationProperty
|
||||
public OrgConfig org;
|
||||
public OrganizationsConfig org;
|
||||
@NestedConfigurationProperty
|
||||
public ApiConfig api;
|
||||
@NestedConfigurationProperty
|
||||
@ -86,7 +86,7 @@ public class HangarConfig {
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public HangarConfig(FakeUserConfig fakeUser, HomepageConfig homepage, ChannelsConfig channels, PagesConfig pages, ProjectsConfig projects, UserConfig user, OrgConfig org, ApiConfig api, SsoConfig sso, HangarSecurityConfig security, QueueConfig queue) {
|
||||
public HangarConfig(FakeUserConfig fakeUser, HomepageConfig homepage, ChannelsConfig channels, PagesConfig pages, ProjectsConfig projects, UserConfig user, OrganizationsConfig org, ApiConfig api, SsoConfig sso, HangarSecurityConfig security, QueueConfig queue) {
|
||||
this.fakeUser = fakeUser;
|
||||
this.homepage = homepage;
|
||||
this.channels = channels;
|
||||
@ -208,7 +208,7 @@ public class HangarConfig {
|
||||
return user;
|
||||
}
|
||||
|
||||
public OrgConfig getOrg() {
|
||||
public OrganizationsConfig getOrg() {
|
||||
return org;
|
||||
}
|
||||
|
||||
|
@ -42,46 +42,6 @@ public class HangarSecurityConfig {
|
||||
private String avatarUrl = "http://localhost:8000/avatar/%s?size=120x120";
|
||||
private String key = "changeme";
|
||||
private long timeout = 10000;
|
||||
@NestedConfigurationProperty
|
||||
public SecurityApiConfig.ApiBreakerConfig breaker;
|
||||
|
||||
@Autowired
|
||||
public SecurityApiConfig(SecurityApiConfig.ApiBreakerConfig breaker) {
|
||||
this.breaker = breaker;
|
||||
}
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.security.api.breaker")
|
||||
public static class ApiBreakerConfig {
|
||||
|
||||
private int maxFailures = 5;
|
||||
private String timeout = "10s";
|
||||
private String reset = "5m";
|
||||
|
||||
public int getMaxFailures() {
|
||||
return maxFailures;
|
||||
}
|
||||
|
||||
public void setMaxFailures(int maxFailures) {
|
||||
this.maxFailures = maxFailures;
|
||||
}
|
||||
|
||||
public String getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(String timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public String getReset() {
|
||||
return reset;
|
||||
}
|
||||
|
||||
public void setReset(String reset) {
|
||||
this.reset = reset;
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
@ -114,14 +74,6 @@ public class HangarSecurityConfig {
|
||||
public void setTimeout(long timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public SecurityApiConfig.ApiBreakerConfig getBreaker() {
|
||||
return breaker;
|
||||
}
|
||||
|
||||
public void setBreaker(SecurityApiConfig.ApiBreakerConfig breaker) {
|
||||
this.breaker = breaker;
|
||||
}
|
||||
}
|
||||
|
||||
public Duration getTokenExpiry() {
|
||||
|
@ -1,13 +1,17 @@
|
||||
package io.papermc.hangar.config.hangar;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.convert.DurationUnit;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.homepage")
|
||||
public class HomepageConfig {
|
||||
|
||||
@DurationUnit(ChronoUnit.MINUTES)
|
||||
private Duration updateInterval = Duration.ofMinutes(10);
|
||||
|
||||
public Duration getUpdateInterval() {
|
||||
|
@ -5,7 +5,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.orgs")
|
||||
public class OrgConfig {
|
||||
public class OrganizationsConfig {
|
||||
private boolean enabled = true;
|
||||
private String dummyEmailDomain = "org.papermc.io";
|
||||
private int createLimit = 5;
|
@ -1,25 +1,29 @@
|
||||
package io.papermc.hangar.config.hangar;
|
||||
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.pages")
|
||||
public class PagesConfig {
|
||||
|
||||
@NestedConfigurationProperty
|
||||
public Home home;
|
||||
private String nameRegex = "^[a-zA-Z0-9-_ ]+$";
|
||||
private int minNameLen = 3;
|
||||
private int maxNameLen = 25;
|
||||
private int minLen = 15;
|
||||
private int maxLen = 32000;
|
||||
@NestedConfigurationProperty
|
||||
public Page page;
|
||||
|
||||
@Autowired
|
||||
public PagesConfig(Home home, Page page) {
|
||||
public PagesConfig(Home home) {
|
||||
this.home = home;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
@Component
|
||||
@ -45,20 +49,6 @@ public class PagesConfig {
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.pages.page")
|
||||
public static class Page {
|
||||
private int maxLen = 75000;
|
||||
|
||||
public int getMaxLen() {
|
||||
return maxLen;
|
||||
}
|
||||
|
||||
public void setMaxLen(int maxLen) {
|
||||
this.maxLen = maxLen;
|
||||
}
|
||||
}
|
||||
|
||||
public Home getHome() {
|
||||
return home;
|
||||
}
|
||||
@ -67,6 +57,30 @@ public class PagesConfig {
|
||||
this.home = home;
|
||||
}
|
||||
|
||||
public String getNameRegex() {
|
||||
return nameRegex;
|
||||
}
|
||||
|
||||
public void setNameRegex(String nameRegex) {
|
||||
this.nameRegex = nameRegex;
|
||||
}
|
||||
|
||||
public int getMinNameLen() {
|
||||
return minNameLen;
|
||||
}
|
||||
|
||||
public void setMinNameLen(int minNameLen) {
|
||||
this.minNameLen = minNameLen;
|
||||
}
|
||||
|
||||
public int getMaxNameLen() {
|
||||
return maxNameLen;
|
||||
}
|
||||
|
||||
public void setMaxNameLen(int maxNameLen) {
|
||||
this.maxNameLen = maxNameLen;
|
||||
}
|
||||
|
||||
public int getMinLen() {
|
||||
return minLen;
|
||||
}
|
||||
@ -83,11 +97,13 @@ public class PagesConfig {
|
||||
this.maxLen = maxLen;
|
||||
}
|
||||
|
||||
public Page getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(Page page) {
|
||||
this.page = page;
|
||||
public void testPageName(String name) {
|
||||
if (name.length() > maxNameLen) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.maxLength");
|
||||
} else if (name.length() < minNameLen) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.minLength");
|
||||
} else if (!Pattern.compile(nameRegex).matcher(name).matches()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.invalidChars");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import java.util.List;
|
||||
@ConfigurationProperties(prefix = "hangar.users")
|
||||
public class UserConfig {
|
||||
private int maxTaglineLen = 100;
|
||||
@Deprecated
|
||||
@Deprecated(forRemoval = true)
|
||||
private int authorPageSize = 25;
|
||||
private List<String> staffRoles = List.of("Hangar_Admin", "Hangar_Mod");
|
||||
|
||||
|
@ -4,6 +4,7 @@ import io.papermc.hangar.controller.HangarController;
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.IProjectsController;
|
||||
import io.papermc.hangar.exceptions.InternalHangarException;
|
||||
import io.papermc.hangar.model.api.PaginatedResult;
|
||||
import io.papermc.hangar.model.api.project.DayProjectStats;
|
||||
import io.papermc.hangar.model.api.project.Project;
|
||||
import io.papermc.hangar.model.api.project.ProjectMember;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
@ -19,7 +20,9 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
public class ProjectsController extends HangarController implements IProjectsController {
|
||||
@ -48,6 +51,12 @@ public class ProjectsController extends HangarController implements IProjectsCon
|
||||
return ResponseEntity.ok(projectsApiService.getProjects(q, categories, tags, owner, sort, orderWithRelevance, pagination));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Map<String, DayProjectStats>> getProjectStats(String author, String slug, @NotNull OffsetDateTime fromDate, @NotNull OffsetDateTime toDate) {
|
||||
return ResponseEntity.ok(projectsApiService.getProjectStats(author, slug, fromDate, toDate));
|
||||
}
|
||||
|
||||
// TODO move to internal project api
|
||||
@GetMapping(value = "/project/{author}/{name}/icon")
|
||||
public Object getProjectIcon(@PathVariable String author, @PathVariable String name) {
|
||||
try {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.papermc.hangar.controller.api.v1.interfaces;
|
||||
|
||||
import io.papermc.hangar.model.api.PaginatedResult;
|
||||
import io.papermc.hangar.model.api.project.DayProjectStats;
|
||||
import io.papermc.hangar.model.api.project.Project;
|
||||
import io.papermc.hangar.model.api.project.ProjectMember;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
@ -16,14 +17,15 @@ import io.swagger.annotations.Authorization;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Anyone
|
||||
@Api(tags = "Projects", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@ -43,7 +45,6 @@ public interface IProjectsController {
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping( "/projects/{author}/{slug}")
|
||||
// @PreAuthorize("@authenticationService.handleApiRequest(T(io.papermc.hangar.model.common.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.extras.ApiScope).ofProject(#author, #slug))")
|
||||
ResponseEntity<Project> getProject(@ApiParam("The author of the project to return") @PathVariable String author,
|
||||
@ApiParam("The slug of the project to return") @PathVariable String slug);
|
||||
|
||||
@ -60,7 +61,6 @@ public interface IProjectsController {
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping("/projects/{author}/{slug}/members")
|
||||
@PreAuthorize("@authenticationService.handleApiRequest(T(io.papermc.hangar.model.common.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.extras.ApiScope).ofProject(#author, #slug))")
|
||||
ResponseEntity<PaginatedResult<ProjectMember>> getProjectMembers(
|
||||
@ApiParam("The author of the project to return members for") @PathVariable("author") String author,
|
||||
@ApiParam("The slug of the project to return") @PathVariable("slug") String slug,
|
||||
@ -89,4 +89,23 @@ public interface IProjectsController {
|
||||
@ApiParam("If how relevant the project is to the given query should be used when sorting the projects") @RequestParam(defaultValue = "true") boolean relevance,
|
||||
@ApiParam("Pagination information") @NotNull RequestPagination pagination
|
||||
);
|
||||
|
||||
@ApiOperation(
|
||||
value = "Returns the stats for a project",
|
||||
nickname = "showProjectStats",
|
||||
notes = "Returns the stats(downloads, views) for a project per day for a certain date range. Requires the `is_subject_member` permission.",
|
||||
authorizations = @Authorization("Session"),
|
||||
tags = "Projects"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok"),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping("/projects/{author}/{slug}/stats")
|
||||
ResponseEntity<Map<String, DayProjectStats>> getProjectStats(@ApiParam("The author of the project to return the stats for") @PathVariable String author,
|
||||
@ApiParam("The slug of the project to return") @PathVariable String slug,
|
||||
@NotNull @ApiParam(value = "The first date to include in the result", required = true) @RequestParam OffsetDateTime fromDate,
|
||||
@NotNull @ApiParam(value = "The last date to include in the result", required = true) @RequestParam OffsetDateTime toDate
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package io.papermc.hangar.controller.internal;
|
||||
|
||||
import io.papermc.hangar.controller.HangarController;
|
||||
import io.papermc.hangar.controller.api.RefreshResponse;
|
||||
import io.papermc.hangar.exceptions.HangarException;
|
||||
import io.papermc.hangar.model.api.auth.RefreshResponse;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.internal.sso.AuthUser;
|
||||
import io.papermc.hangar.model.internal.sso.URLWithNonce;
|
||||
|
@ -11,8 +11,8 @@ 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.service.internal.MarkdownService;
|
||||
import io.papermc.hangar.service.internal.projects.ProjectPageService;
|
||||
import io.papermc.hangar.serviceold.MarkdownService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -7,7 +7,7 @@ import freemarker.template.TemplateHashModel;
|
||||
import freemarker.template.TemplateModelException;
|
||||
import io.papermc.hangar.config.hangar.HangarConfig;
|
||||
import io.papermc.hangar.db.modelold.UsersTable;
|
||||
import io.papermc.hangar.serviceold.MarkdownService;
|
||||
import io.papermc.hangar.service.internal.MarkdownService;
|
||||
import io.papermc.hangar.serviceold.UserService;
|
||||
import io.papermc.hangar.util.TemplateHelper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -14,7 +14,7 @@ import io.papermc.hangar.modelold.viewhelpers.ProjectPage;
|
||||
import io.papermc.hangar.modelold.viewhelpers.ScopedProjectData;
|
||||
import io.papermc.hangar.securityold.annotations.ProjectPermission;
|
||||
import io.papermc.hangar.securityold.annotations.UserLock;
|
||||
import io.papermc.hangar.serviceold.MarkdownService;
|
||||
import io.papermc.hangar.service.internal.MarkdownService;
|
||||
import io.papermc.hangar.serviceold.StatsService;
|
||||
import io.papermc.hangar.serviceold.UserActionLogService;
|
||||
import io.papermc.hangar.serviceold.project.PagesFactory;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package io.papermc.hangar.controllerold;
|
||||
|
||||
import io.papermc.hangar.serviceold.PaypalService;
|
||||
import io.papermc.hangar.service.internal.PaypalService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
@ -1,71 +0,0 @@
|
||||
package io.papermc.hangar.controllerold.api;
|
||||
|
||||
import io.papermc.hangar.modelold.generated.Project;
|
||||
import io.papermc.hangar.modelold.generated.ProjectStatsDay;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
import io.swagger.annotations.Authorization;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Map;
|
||||
|
||||
@Api(value = "projects", tags = "Projects")
|
||||
@RequestMapping({"/api", "/api/v1"})
|
||||
public interface ProjectsApi {
|
||||
|
||||
@ApiOperation(
|
||||
value = "Returns info on a specific project",
|
||||
nickname = "showProjectById",
|
||||
notes = "Returns info on a specific project. Requires the `view_public_info` permission.",
|
||||
response = Project.class,
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Projects",
|
||||
hidden = true
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok", response = Project.class),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Project> showProject(
|
||||
@ApiParam(value = "The id of the project to return", required = true) @PathVariable("id") long id
|
||||
);
|
||||
|
||||
@ApiOperation(
|
||||
value = "Returns the stats for a project",
|
||||
nickname = "showProjectStats",
|
||||
notes = "Returns the stats(downloads, views) for a project per day for a certain date range. Requires the `is_subject_member` permission.",
|
||||
response = ProjectStatsDay.class,
|
||||
responseContainer = "Map",
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Projects"
|
||||
)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(code = 200, message = "Ok", response = ProjectStatsDay.class, responseContainer = "Map"),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/stats", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Map<String, ProjectStatsDay>> showProjectStats(
|
||||
@ApiParam(value = "The author of the project to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return", required = true) @PathVariable("slug") String slug,
|
||||
@NotNull @ApiParam(value = "The first date to include in the result", required = true) @Valid @RequestParam(value = "fromDate", required = true) String fromDate,
|
||||
@NotNull @ApiParam(value = "The last date to include in the result", required = true) @Valid @RequestParam(value = "toDate", required = true) String toDate
|
||||
);
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package io.papermc.hangar.controllerold.api;
|
||||
|
||||
import io.papermc.hangar.config.hangar.HangarConfig;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.modelold.ApiAuthInfo;
|
||||
import io.papermc.hangar.modelold.generated.Project;
|
||||
import io.papermc.hangar.modelold.generated.ProjectStatsDay;
|
||||
import io.papermc.hangar.serviceold.apiold.ProjectApiService;
|
||||
import io.papermc.hangar.util.ApiUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
public class ProjectsApiController implements ProjectsApi {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ProjectsApiController.class);
|
||||
|
||||
private final HangarConfig hangarConfig;
|
||||
private final ApiAuthInfo apiAuthInfo;
|
||||
private final ProjectApiService projectApiService;
|
||||
|
||||
@Autowired
|
||||
public ProjectsApiController(HangarConfig hangarConfig, ApiAuthInfo apiAuthInfo, ProjectApiService projectApiService) {
|
||||
this.hangarConfig = hangarConfig;
|
||||
this.apiAuthInfo = apiAuthInfo;
|
||||
this.projectApiService = projectApiService;
|
||||
}
|
||||
|
||||
// @Override
|
||||
// @PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.common.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.extras.ApiScope).ofProject(#author, #slug))")
|
||||
// public ResponseEntity<List<ProjectMember>> showMembers(String author, String slug, Long inLimit, Long inOffset) {
|
||||
// long limit = ApiUtil.limitOrDefault(inLimit, hangarConfig.getProjects().getInitLoad());
|
||||
// long offset = ApiUtil.offsetOrZero(inOffset);
|
||||
// List<ProjectMember> projectMembers = projectApiService.getProjectMembers(author, slug, limit, offset);
|
||||
// if (projectMembers == null || projectMembers.isEmpty()) { // TODO this will also happen when the offset is too high
|
||||
// log.error("Couldn't find a project for that author/slug");
|
||||
// throw new HangarApiException(HttpStatus.NOT_FOUND, "Couldn't find a project for: " + author + "/" + slug);
|
||||
// }
|
||||
// return ResponseEntity.ok(projectMembers);
|
||||
// }
|
||||
|
||||
@Override
|
||||
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.common.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.extras.ApiScope).ofProject(#id))")
|
||||
public ResponseEntity<Project> showProject(long id) {
|
||||
boolean seeHidden = apiAuthInfo.getGlobalPerms().has(Permission.SeeHidden);
|
||||
Project project = projectApiService.getProject(id, seeHidden, apiAuthInfo.getUserId());
|
||||
if (project == null) {
|
||||
log.error("Couldn't find a project for that id");
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND, "Couldn't find a project for id: " + id);
|
||||
}
|
||||
return ResponseEntity.ok(project);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.common.Permission).IsProjectMember, T(io.papermc.hangar.controller.extras.ApiScope).ofProject(#author, #slug))")
|
||||
public ResponseEntity<Map<String, ProjectStatsDay>> showProjectStats(String author, String slug, @NotNull @Valid String fromDate, @NotNull @Valid String toDate) {
|
||||
LocalDate from = ApiUtil.parseDate(fromDate);
|
||||
LocalDate to = ApiUtil.parseDate(toDate);
|
||||
if (from.isAfter(to)) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "From date is after to date");
|
||||
}
|
||||
Map<String, ProjectStatsDay> projectStats = projectApiService.getProjectStats(author, slug, from, to);
|
||||
if (projectStats == null || projectStats.isEmpty()) {
|
||||
log.error("Couldn't find a project for that author/slug");
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND, "Couldn't find a project for: " + author + "/" + slug);
|
||||
}
|
||||
return ResponseEntity.ok(projectStats);
|
||||
}
|
||||
}
|
@ -6,10 +6,12 @@ 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.SqlBatch;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -25,6 +27,9 @@ public interface ProjectPagesDAO {
|
||||
@SqlUpdate("UPDATE project_pages SET contents = :contents WHERE id = :id")
|
||||
void update(@BindBean ProjectPageTable projectPageTable);
|
||||
|
||||
@SqlBatch("UPDATE project_pages SET parent_id = :parentId WHERE id = :id")
|
||||
void updateParents(@BindBean Collection<ProjectPageTable> projectPageTables);
|
||||
|
||||
@SqlUpdate("DELETE FROM project_pages WHERE id = :id")
|
||||
void delete(@BindBean ProjectPageTable projectPageTable);
|
||||
|
||||
@ -32,8 +37,11 @@ public interface ProjectPagesDAO {
|
||||
@SqlQuery("SELECT * FROM project_pages WHERE project_id = :projectId AND parent_id IS NULL")
|
||||
Map<Long, ProjectPageTable> getRootPages(long projectId);
|
||||
|
||||
@SqlQuery("SELECT slug FROM project_pages WHERE project_id = :projectId AND parent_id = :parentId")
|
||||
List<String> getChildPages(long projectId, long parentId);
|
||||
@SqlQuery("SELECT * FROM project_pages WHERE project_id = :projectId AND parent_id = :parentId")
|
||||
List<ProjectPageTable> getChildPages(long projectId, long parentId);
|
||||
|
||||
@SqlQuery("SELECT * FROM project_pages WHERE project_id = :projectId AND parent_id = :parentId AND slug = :slug")
|
||||
ProjectPageTable getChildPage(long projectId, long parentId, String slug);
|
||||
|
||||
@SqlQuery("SELECT pp.* FROM project_pages pp" +
|
||||
" WHERE pp.project_id = :projectId" +
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.papermc.hangar.db.dao.v1;
|
||||
|
||||
import io.papermc.hangar.db.mappers.PromotedVersionMapper;
|
||||
import io.papermc.hangar.model.api.project.DayProjectStats;
|
||||
import io.papermc.hangar.model.api.project.Project;
|
||||
import io.papermc.hangar.model.api.project.ProjectMember;
|
||||
import org.jdbi.v3.sqlobject.config.KeyColumn;
|
||||
@ -14,7 +15,9 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
@RegisterConstructorMapper(Project.class)
|
||||
@ -110,4 +113,18 @@ public interface ProjectsApiDAO {
|
||||
" WHERE p.slug = :slug AND p.owner_name = :author " +
|
||||
" GROUP BY u.name")
|
||||
long getProjectMembersCount(String author, String slug);
|
||||
|
||||
@KeyColumn("dateKey")
|
||||
@RegisterConstructorMapper(DayProjectStats.class)
|
||||
@SqlQuery("SELECT cast(dates.day AS date) dateKey, coalesce(sum(pvd.downloads), 0) AS downloads, coalesce(pv.views, 0) AS views" +
|
||||
" FROM projects p," +
|
||||
" (SELECT generate_series(:fromDate::date, :toDate::date, INTERVAL '1 DAY') as day) dates " +
|
||||
" LEFT JOIN project_versions_downloads pvd ON dates.day = pvd.day" +
|
||||
" LEFT JOIN project_views pv ON dates.day = pv.day AND pvd.project_id = pv.project_id" +
|
||||
" WHERE " +
|
||||
" p.owner_name = :author AND " +
|
||||
" p.slug = :slug AND" +
|
||||
" (pvd IS NULL OR pvd.project_id = p.id)" +
|
||||
" GROUP BY pv.views, dates.day")
|
||||
Map<String, DayProjectStats> getProjectStats(String author, String slug, OffsetDateTime fromDate, OffsetDateTime toDate);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.papermc.hangar.controller.api;
|
||||
package io.papermc.hangar.model.api.auth;
|
||||
|
||||
public class RefreshResponse {
|
||||
|
@ -0,0 +1,28 @@
|
||||
package io.papermc.hangar.model.api.project;
|
||||
|
||||
public class DayProjectStats {
|
||||
|
||||
private final long views;
|
||||
private final long downloads;
|
||||
|
||||
public DayProjectStats(long views, long downloads) {
|
||||
this.views = views;
|
||||
this.downloads = downloads;
|
||||
}
|
||||
|
||||
public long getViews() {
|
||||
return views;
|
||||
}
|
||||
|
||||
public long getDownloads() {
|
||||
return downloads;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DayProjectStats{" +
|
||||
"views=" + views +
|
||||
", downloads=" + downloads +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ public class ProjectPageTable extends Table implements Named {
|
||||
private final String slug;
|
||||
private String contents;
|
||||
private final boolean deletable;
|
||||
private final Long parentId;
|
||||
private Long parentId;
|
||||
|
||||
@JdbiConstructor
|
||||
public ProjectPageTable(OffsetDateTime createdAt, long id, long projectId, String name, String slug, String contents, boolean deletable, Long parentId) {
|
||||
@ -67,6 +67,10 @@ public class ProjectPageTable extends Table implements Named {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(Long parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProjectPageTable{" +
|
||||
|
@ -1,12 +1,16 @@
|
||||
package io.papermc.hangar.model.internal.api.requests.projects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import io.papermc.hangar.controller.validations.Validate;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
public class NewProjectPage {
|
||||
|
||||
@NotBlank
|
||||
@Validate(SpEL = "#root.length() ge @hangarConfig.pages.minNameLen", message = "page.new.error.name.minLength")
|
||||
@Validate(SpEL = "#root.length() le @hangarConfig.pages.maxNameLen", message = "page.new.error.name.maxLength")
|
||||
@Validate(SpEL = "#root matches @hangarConfig.pages.nameRegex", message = "page.new.error.name.invalidChars")
|
||||
private final String name;
|
||||
private final Long parentId;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package io.papermc.hangar.model.internal.sso;
|
||||
|
||||
import io.papermc.hangar.model.common.roles.GlobalRole;
|
||||
import io.papermc.hangar.util.SsoUtil;
|
||||
import io.papermc.hangar.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -71,7 +71,7 @@ public class SsoSyncData {
|
||||
Integer.parseInt(payload.get("external_id")),
|
||||
payload.get("email"),
|
||||
payload.get("username"),
|
||||
SsoUtil.parsePythonNullable(payload.get("name")),
|
||||
StringUtils.parsePythonNullable(payload.get("name")),
|
||||
payload.get("add_groups").split(","),
|
||||
payload.get("remove_groups").split(","),
|
||||
Boolean.parseBoolean(payload.get("admin")),
|
||||
|
@ -7,7 +7,7 @@ import io.papermc.hangar.db.modelold.ProjectsTable;
|
||||
import io.papermc.hangar.db.modelold.UserProjectRolesTable;
|
||||
import io.papermc.hangar.db.modelold.UsersTable;
|
||||
import io.papermc.hangar.model.common.projects.Visibility;
|
||||
import io.papermc.hangar.serviceold.MarkdownService;
|
||||
import io.papermc.hangar.service.internal.MarkdownService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -37,7 +37,7 @@ public class UnlockedVoter implements AccessDecisionVoter<MethodInvocation> {
|
||||
if (((HangarAuthenticationToken) authentication).getPrincipal().isLocked()) {
|
||||
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "error.user.locked");
|
||||
}
|
||||
return ACCESS_ABSTAIN;
|
||||
return ACCESS_GRANTED;
|
||||
}
|
||||
|
||||
private UnlockedAttribute findUnlockedAttribute(Collection<ConfigAttribute> attributes) {
|
||||
|
@ -61,6 +61,8 @@ public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
|
||||
decisionVoters.add(roleVoter);
|
||||
decisionVoters.add(new AuthenticatedVoter());
|
||||
decisionVoters.addAll(accessDecisionVoters);
|
||||
return new UnanimousBased(decisionVoters);
|
||||
UnanimousBased accessDecisionManager = new UnanimousBased(decisionVoters);
|
||||
accessDecisionManager.setAllowIfAllAbstainDecisions(true);
|
||||
return accessDecisionManager;
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import io.papermc.hangar.controller.api.RefreshResponse;
|
||||
import io.papermc.hangar.controller.utils.CookieUtils;
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.UserRefreshTokenDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.auth.RefreshResponse;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.db.auth.UserRefreshToken;
|
||||
|
@ -4,6 +4,7 @@ import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.v1.ProjectsApiDAO;
|
||||
import io.papermc.hangar.model.api.PaginatedResult;
|
||||
import io.papermc.hangar.model.api.Pagination;
|
||||
import io.papermc.hangar.model.api.project.DayProjectStats;
|
||||
import io.papermc.hangar.model.api.project.Project;
|
||||
import io.papermc.hangar.model.api.project.ProjectMember;
|
||||
import io.papermc.hangar.model.api.requests.RequestPagination;
|
||||
@ -12,12 +13,13 @@ import io.papermc.hangar.model.common.projects.Category;
|
||||
import io.papermc.hangar.modelold.generated.ProjectSortingStrategy;
|
||||
import io.papermc.hangar.modelold.generated.Tag;
|
||||
import io.papermc.hangar.service.HangarService;
|
||||
import io.papermc.hangar.util.ApiUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@ -26,8 +28,8 @@ public class ProjectsApiService extends HangarService {
|
||||
private final ProjectsApiDAO projectsApiDAO;
|
||||
|
||||
@Autowired
|
||||
public ProjectsApiService(HangarDao<ProjectsApiDAO> projectsDAO) {
|
||||
this.projectsApiDAO = projectsDAO.get();
|
||||
public ProjectsApiService(HangarDao<ProjectsApiDAO> projectsApiDAO) {
|
||||
this.projectsApiDAO = projectsApiDAO.get();
|
||||
}
|
||||
|
||||
public Project getProject(String author, String slug) {
|
||||
@ -40,6 +42,10 @@ public class ProjectsApiService extends HangarService {
|
||||
return new PaginatedResult<>(new Pagination(projectsApiDAO.getProjectMembersCount(author, slug), requestPagination), projectMembers);
|
||||
}
|
||||
|
||||
public Map<String, DayProjectStats> getProjectStats(String author, String slug, OffsetDateTime fromDate, OffsetDateTime toDate) {
|
||||
return projectsApiDAO.getProjectStats(author, slug, fromDate, toDate);
|
||||
}
|
||||
|
||||
public PaginatedResult<Project> getProjects(String query, List<Category> categories, List<String> tags, String owner, ProjectSortingStrategy sort, boolean orderWithRelevance, RequestPagination pagination) {
|
||||
List<Tag> parsedTags = new ArrayList<>();
|
||||
if (tags == null) {
|
||||
@ -51,7 +57,7 @@ public class ProjectsApiService extends HangarService {
|
||||
}
|
||||
boolean seeHidden = getHangarPrincipal().getGlobalPermissions().has(Permission.SeeHidden);
|
||||
|
||||
String ordering = ApiUtil.strategyOrDefault(sort).getSql();
|
||||
String ordering = sort.getSql();
|
||||
if (orderWithRelevance && query != null && !query.isEmpty()) {
|
||||
String relevance = "ts_rank(p.search_words, websearch_to_tsquery_postfix('english', :query)) DESC";
|
||||
if(query.endsWith(" ")) {
|
||||
@ -61,7 +67,7 @@ public class ProjectsApiService extends HangarService {
|
||||
// 1483056000 is the Ore epoch TODO change to hangar epoch
|
||||
// 86400 seconds to days
|
||||
// 604800 seconds to weeks
|
||||
switch(ApiUtil.strategyOrDefault(sort)){
|
||||
switch(sort){
|
||||
case STARS: orderingFirstHalf = "p.starts * "; break;
|
||||
case DOWNLOADS: orderingFirstHalf ="(p.downloads / 100) * "; break;
|
||||
case VIEWS: orderingFirstHalf ="(p.views / 200) *"; break;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.papermc.hangar.serviceold;
|
||||
package io.papermc.hangar.service.internal;
|
||||
|
||||
import com.vladsch.flexmark.ast.MailLink;
|
||||
import com.vladsch.flexmark.ext.admonition.AdmonitionExtension;
|
@ -1,5 +1,6 @@
|
||||
package io.papermc.hangar.serviceold;
|
||||
package io.papermc.hangar.service.internal;
|
||||
|
||||
import io.papermc.hangar.serviceold.HangarService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
@ -15,6 +15,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@ -32,11 +33,13 @@ public class ProjectPageService extends HangarService {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.minLength");
|
||||
}
|
||||
|
||||
if (!hangarConfig.projects.getNameMatcher().test(name)) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.invalidName");
|
||||
if (contents.length() > hangarConfig.pages.getMaxLen()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.maxLength");
|
||||
}
|
||||
|
||||
if (parentId != null && projectPagesDAO.getChildPages(projectId, parentId).contains(slug)) {
|
||||
hangarConfig.pages.testPageName(name);
|
||||
|
||||
if (parentId != null && projectPagesDAO.getChildPage(projectId, parentId, slug) != null) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.duplicateName");
|
||||
}
|
||||
|
||||
@ -126,6 +129,13 @@ public class ProjectPageService extends HangarService {
|
||||
if (!pageTable.isDeletable()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "Page is not deletable");
|
||||
}
|
||||
List<ProjectPageTable> children = projectPagesDAO.getChildPages(projectId, pageId);
|
||||
if (!children.isEmpty()) {
|
||||
for (ProjectPageTable child : children) {
|
||||
child.setParentId(pageTable.getParentId());
|
||||
}
|
||||
projectPagesDAO.updateParents(children);
|
||||
}
|
||||
// Log must come first otherwise db error
|
||||
userActionLogService.projectPage(LoggedActionType.PROJECT_PAGE_DELETED.with(ProjectPageContext.of(projectId, pageId)), "", pageTable.getContents());
|
||||
projectPagesDAO.delete(pageTable);
|
||||
|
@ -2,10 +2,7 @@ package io.papermc.hangar.serviceold.apiold;
|
||||
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.daoold.api.UsersApiDao;
|
||||
import io.papermc.hangar.modelold.generated.ProjectCompact;
|
||||
import io.papermc.hangar.modelold.generated.ProjectSortingStrategy;
|
||||
import io.papermc.hangar.modelold.generated.User;
|
||||
import io.papermc.hangar.util.ApiUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
@ -30,20 +27,4 @@ public class UserApiService {
|
||||
public long getUsersCount(String q) {
|
||||
return usersApiDao.get().usersQueryCount(q);
|
||||
}
|
||||
|
||||
public List<ProjectCompact> getWatchedProjects(String user, boolean seeHidden, Long userId, ProjectSortingStrategy strategy, long limit, long offset) {
|
||||
return usersApiDao.get().watchersQuery(user, seeHidden, userId, ApiUtil.strategyOrDefault(strategy).getSql(), limit, offset);
|
||||
}
|
||||
|
||||
public long getWatchedProjectsCount(String user, boolean seeHidden, Long userId) {
|
||||
return usersApiDao.get().watchersQuery(user, seeHidden, userId, ProjectSortingStrategy.Default.getSql(), null, 0).size();
|
||||
}
|
||||
|
||||
public List<ProjectCompact> getStarredProjects(String user, boolean seeHidden, Long userId, ProjectSortingStrategy strategy, long limit, long offset) {
|
||||
return usersApiDao.get().starredQuery(user, seeHidden, userId, ApiUtil.strategyOrDefault(strategy).getSql(), limit, offset);
|
||||
}
|
||||
|
||||
public long getStarredProjectsCount(String user, boolean seeHidden, Long userId) {
|
||||
return usersApiDao.get().starredQuery(user, seeHidden, userId, ProjectSortingStrategy.Default.getSql(), null, 0).size();
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
package io.papermc.hangar.serviceold.sso;
|
||||
|
||||
public class UrlWithNonce {
|
||||
|
||||
private final String url;
|
||||
private final String nonce;
|
||||
|
||||
public UrlWithNonce(String url, String nonce) {
|
||||
this.url = url;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package io.papermc.hangar.template;
|
||||
|
||||
public class HangarDirectives {
|
||||
|
||||
|
||||
}
|
@ -7,6 +7,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
public class AlertUtil {
|
||||
|
||||
public enum AlertType {
|
||||
|
@ -1,43 +0,0 @@
|
||||
package io.papermc.hangar.util;
|
||||
|
||||
import io.papermc.hangar.db.modelold.UsersTable;
|
||||
import io.papermc.hangar.modelold.generated.ProjectSortingStrategy;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
public class ApiUtil {
|
||||
|
||||
private ApiUtil() { }
|
||||
|
||||
@Deprecated
|
||||
public static long limitOrDefault(Long limit, long defaultValue) {
|
||||
if (limit != null && limit < 1) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Limit should be greater than 0");
|
||||
return Math.min(limit == null ? defaultValue : limit, defaultValue);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static long offsetOrZero(Long offset) {
|
||||
return Math.max(offset == null ? 0 : offset, 0);
|
||||
}
|
||||
|
||||
public static Long userIdOrNull(UsersTable usersTable) {
|
||||
return usersTable == null ? null : usersTable.getId();
|
||||
}
|
||||
|
||||
public static LocalDate parseDate(String date) {
|
||||
try {
|
||||
return LocalDate.parse(date);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Badly formatted date " + date);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static ProjectSortingStrategy strategyOrDefault(@Nullable ProjectSortingStrategy strategy) {
|
||||
return strategy == null ? ProjectSortingStrategy.Default : strategy;
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Deprecated
|
||||
public class ListUtils {
|
||||
|
||||
private ListUtils() { }
|
||||
|
@ -1,10 +1,6 @@
|
||||
package io.papermc.hangar.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
@ -12,10 +8,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class RequestUtil {
|
||||
|
||||
@ -38,28 +32,8 @@ public class RequestUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> Optional<T> requirePathParams(List<String> requestedParams, Function<String[], T> tFunction) {
|
||||
Map<String, String> pathParams = getCurrentPathParams();
|
||||
if (pathParams.keySet().containsAll(requestedParams)) {
|
||||
return Optional.ofNullable(tFunction.apply(requestedParams.toArray(new String[0])));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static Map<String, String> getCurrentPathParams() {
|
||||
return getPathParams(getCurrentRequest());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static Map<String, String> getPathParams(HttpServletRequest request) {
|
||||
return Optional.ofNullable((Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)).orElse(new HashMap<>());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static HttpServletRequest getCurrentRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
throw new IllegalStateException("Cannot get http servlet request out of HTTP context");
|
||||
}
|
||||
return (HttpServletRequest) requestAttributes;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package io.papermc.hangar.util;
|
||||
|
||||
public class SsoUtil {
|
||||
public static String parsePythonNullable(String input) {
|
||||
return input.equals("None") ? null : input;
|
||||
}
|
||||
}
|
@ -163,4 +163,8 @@ public class StringUtils {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String parsePythonNullable(String input) {
|
||||
return input.equals("None") ? null : input;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import java.time.format.FormatStyle;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Deprecated
|
||||
public class TemplateHelper {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
@ -45,7 +45,6 @@ hangar:
|
||||
dev: true
|
||||
auth-url: "http://localhost:8000"
|
||||
base-url: "http://localhost:8080"
|
||||
use-webpack: true
|
||||
ga-code: "UA-38006759-9"
|
||||
|
||||
announcements:
|
||||
@ -70,7 +69,7 @@ hangar:
|
||||
channels:
|
||||
max-name-len: 15
|
||||
name-regex: "^[a-zA-Z0-9]+$"
|
||||
color-default: 7
|
||||
color-default: cyan
|
||||
name-default: "Release"
|
||||
|
||||
pages:
|
||||
@ -78,9 +77,10 @@ hangar:
|
||||
name: "Home"
|
||||
message: "Welcome to your new project!"
|
||||
min-len: 15
|
||||
max-len: 32000
|
||||
page:
|
||||
max-len: 75000
|
||||
max-len: 75000
|
||||
name-regex: "^[a-zA-Z0-9-_ ]+$"
|
||||
max-name-len: 25
|
||||
min-name-len: 3
|
||||
|
||||
projects:
|
||||
max-name-len: 25
|
||||
@ -98,6 +98,11 @@ hangar:
|
||||
unsafe-download-max-age: 10
|
||||
name-regex: "^[a-zA-Z0-9-_]{3,}$"
|
||||
|
||||
orgs:
|
||||
create-limit: 5
|
||||
dummy-email-domain: "org.papermc.io"
|
||||
enabled: true
|
||||
|
||||
users:
|
||||
max-tagline-len: 100
|
||||
author-page-size: 25
|
||||
@ -133,9 +138,5 @@ hangar:
|
||||
avatar-url: "https://paper.readthedocs.io/en/latest/_images/papermc_logomark_500.png"
|
||||
key: changeme
|
||||
timeout: 10000
|
||||
breaker:
|
||||
max-failures: 5
|
||||
timeout: 10s
|
||||
reset: 5m
|
||||
safe-download-hosts:
|
||||
- "github.com"
|
||||
|
@ -125,7 +125,7 @@ CREATE TABLE project_pages
|
||||
parent_id bigint
|
||||
CONSTRAINT project_pages_parent_id_fkey
|
||||
REFERENCES project_pages
|
||||
ON DELETE SET NULL
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX page_slug_idx
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.papermc.hangar.serviceold;
|
||||
package io.papermc.hangar.service.internal;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
Loading…
x
Reference in New Issue
Block a user