mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-31 16:00:39 +08:00
delete page
This commit is contained in:
parent
582785a71f
commit
9b8e45ca08
@ -12,8 +12,7 @@ fake-user:
|
||||
enabled: false
|
||||
|
||||
hangar:
|
||||
debug: true
|
||||
use-webpack: false
|
||||
dev: false
|
||||
auth-url: "https://hangar-new-auth.minidigger.me"
|
||||
base-url: "https://hangar-new.minidigger.me"
|
||||
plugin-upload-dir: "/hangar/uploads"
|
||||
|
@ -33,9 +33,7 @@ fake-user:
|
||||
email: paper@papermc.io
|
||||
|
||||
hangar:
|
||||
debug: true
|
||||
debug-level: 3
|
||||
log-timings: false
|
||||
dev: true
|
||||
auth-url: "http://localhost:8000"
|
||||
base-url: "http://localhost:8080"
|
||||
plugin-upload-dir: "/uploads"
|
||||
@ -84,10 +82,8 @@ hangar:
|
||||
name-regex: "^[a-zA-Z0-9-_]{3,}$"
|
||||
|
||||
users:
|
||||
stars-per-page: 5
|
||||
max-tagline-len: 100
|
||||
author-page-size: 25
|
||||
project-page-size: 5
|
||||
staff-roles:
|
||||
- Hangar_Admin
|
||||
- Hangar_Mod
|
||||
@ -117,7 +113,6 @@ hangar:
|
||||
api:
|
||||
url: "http://auth:8000"
|
||||
avatar-url: "http://localhost:8000/avatar/%s?size=120x120" # only comment in if you run auth locally
|
||||
# avatar-url: "https://paper.readthedocs.io/en/latest/_images/papermc_logomark_500.png"
|
||||
key: changeme
|
||||
timeout: 10000
|
||||
breaker:
|
||||
|
@ -4,7 +4,7 @@ module.exports = {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['@nuxtjs/eslint-config-typescript', 'prettier', 'prettier/vue'],
|
||||
extends: ['plugin:nuxt/recommended', 'plugin:prettier/recommended', '@nuxtjs/eslint-config-typescript', 'prettier', 'prettier/vue'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
|
@ -6,4 +6,8 @@
|
||||
|
||||
@mixin basic-border() {
|
||||
border: 1px solid $lighter;
|
||||
}
|
||||
|
||||
@mixin undoCapitalization() {
|
||||
text-transform: unset;
|
||||
}
|
@ -17,9 +17,24 @@
|
||||
<v-btn v-show="isEditing && preview" class="page-btn preview-btn info" fab absolute icon x-small @click="preview = false">
|
||||
<v-icon>mdi-eye-off</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-show="isEditing && deletable" class="page-btn delete-btn error" fab absolute icon x-small :loading="loading.delete" @click="deletePage">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<DeletePageModal @delete="deletePage">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
v-show="isEditing && deletable"
|
||||
v-bind="attrs"
|
||||
class="page-btn delete-btn error"
|
||||
fab
|
||||
absolute
|
||||
icon
|
||||
x-small
|
||||
:loading="loading.delete"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</DeletePageModal>
|
||||
|
||||
<v-btn
|
||||
v-show="isEditing"
|
||||
class="page-btn cancel-btn warning red darken-2"
|
||||
@ -40,9 +55,11 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, PropSync, Vue, Watch } from 'nuxt-property-decorator';
|
||||
import Markdown from '~/components/Markdown.vue';
|
||||
import DeletePageModal from '~/components/modals/pages/DeletePageModal.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
DeletePageModal,
|
||||
Markdown,
|
||||
},
|
||||
})
|
||||
@ -75,12 +92,12 @@ export default class MarkdownEditor extends Vue {
|
||||
deletePage() {
|
||||
this.loading.delete = true;
|
||||
this.$emit('delete');
|
||||
// TODO implement on parent
|
||||
}
|
||||
|
||||
@Watch('isEditing')
|
||||
onEditChange(value: boolean) {
|
||||
if (!value) {
|
||||
this.preview = false;
|
||||
this.loading.save = false;
|
||||
this.loading.delete = false;
|
||||
}
|
||||
|
75
frontend/components/mixins.ts
Normal file
75
frontend/components/mixins.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator';
|
||||
import { PropType } from 'vue';
|
||||
import { HangarProject, ProjectPage } from 'hangar-internal';
|
||||
import MarkdownEditor from '~/components/MarkdownEditor.vue';
|
||||
import { NamedPermission } from '~/types/enums';
|
||||
|
||||
@Component
|
||||
export class HangarProjectMixin extends Vue {
|
||||
@Prop({ type: Object as PropType<HangarProject>, required: true })
|
||||
project!: HangarProject;
|
||||
}
|
||||
|
||||
@Component
|
||||
export class DocPageMixin extends HangarProjectMixin {
|
||||
editingPage: boolean = false;
|
||||
page = {
|
||||
contents: '',
|
||||
deletable: false,
|
||||
} as ProjectPage;
|
||||
|
||||
$refs!: {
|
||||
editor: MarkdownEditor;
|
||||
};
|
||||
|
||||
get canEdit(): boolean {
|
||||
return this.$util.hasPerms(NamedPermission.EDIT_PAGE);
|
||||
}
|
||||
|
||||
savePage(content: string) {
|
||||
this.$api
|
||||
.requestInternal(`pages/save/${this.project.id}/${this.page.id}`, true, 'post', {
|
||||
content,
|
||||
})
|
||||
.then(() => {
|
||||
this.page.contents = content;
|
||||
this.editingPage = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$refs.editor.loading.save = false;
|
||||
this.$util.handleRequestError(err, 'Unable to save page');
|
||||
});
|
||||
}
|
||||
|
||||
deletePage() {
|
||||
this.$api
|
||||
.requestInternal(`pages/delete/${this.project.id}/${this.page.id}`, true, 'post')
|
||||
.then(() => {
|
||||
this.$refs.editor.loading.delete = false;
|
||||
this.$router.replace(`/${this.$route.params.author}/${this.$route.params.slug}`);
|
||||
})
|
||||
.catch(this.$util.handleRequestError);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
export class HangarModal extends Vue {
|
||||
dialog: boolean = false;
|
||||
|
||||
@Prop({ type: String, default: '' })
|
||||
activatorClass!: string;
|
||||
|
||||
@Watch('dialog')
|
||||
onToggleView() {
|
||||
if (typeof this.$refs.modalForm !== 'undefined') {
|
||||
// @ts-ignore
|
||||
this.$refs.modalForm.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
export class HangarFormModal extends HangarModal {
|
||||
loading: boolean = false;
|
||||
validForm: boolean = false;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-dialog v-model="shown" width="500" persistent>
|
||||
<v-dialog v-model="dialog" width="500" persistent>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on">
|
||||
<v-btn v-bind="attrs" :class="activatorClass" v-on="on">
|
||||
<v-icon>mdi-flag</v-icon>
|
||||
{{ $t('project.actions.flag') }}
|
||||
</v-btn>
|
||||
@ -9,7 +9,7 @@
|
||||
<v-card>
|
||||
<v-card-title> {{ $t('project.flag.flagProject', [project.name]) }} </v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="flagForm" v-model="form.valid">
|
||||
<v-form ref="modalForm" v-model="validForm">
|
||||
<v-radio-group v-model="form.selection" :rules="[$util.$vc.require('A reason')]">
|
||||
<v-radio v-for="(reason, index) in flagReasons" :key="index" :label="reason.title" :value="reason.type" />
|
||||
</v-radio-group>
|
||||
@ -17,53 +17,43 @@
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn text color="warning" @click.stop="shown = false">{{ $t('general.close') }}</v-btn>
|
||||
<v-btn color="error" :disabled="!form.valid" :loading="loading" @click.stop="submitFlag">{{ $t('general.submit') }}</v-btn>
|
||||
<v-btn text color="warning" @click.stop="dialog = false">{{ $t('general.close') }}</v-btn>
|
||||
<v-btn color="error" :disabled="!validForm" :loading="loading" @click.stop="submitFlag">{{ $t('general.submit') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'nuxt-property-decorator';
|
||||
import { Prop } from 'vue-property-decorator';
|
||||
import { Component, Prop } from 'nuxt-property-decorator';
|
||||
import { FlagReason, HangarProject } from 'hangar-internal';
|
||||
import { PropType } from 'vue';
|
||||
import { HangarFormModal } from '~/components/mixins';
|
||||
|
||||
@Component
|
||||
export default class FlagModal extends Vue {
|
||||
export default class FlagModal extends HangarFormModal {
|
||||
flagReasons: FlagReason[] = [];
|
||||
shown = false;
|
||||
loading = false;
|
||||
form = {
|
||||
valid: false,
|
||||
selection: null as string | null,
|
||||
comment: null as string | null,
|
||||
};
|
||||
|
||||
@Prop({ required: true })
|
||||
@Prop({ required: true, type: Object as PropType<HangarProject> })
|
||||
project!: HangarProject;
|
||||
|
||||
submitFlag() {
|
||||
this.loading = true;
|
||||
// TODO endpoint
|
||||
// TODO flag endpoint
|
||||
setTimeout(
|
||||
(self: FlagModal) => {
|
||||
self.loading = false;
|
||||
self.shown = false;
|
||||
self.dialog = false;
|
||||
},
|
||||
1000,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
@Watch('shown')
|
||||
onToggle() {
|
||||
if (this.$refs.flagForm) {
|
||||
// @ts-ignore // TODO how to fix this?
|
||||
this.$refs.flagForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
this.flagReasons.push(...(await this.$api.requestInternal<FlagReason[]>('data/flagReasons', true)));
|
||||
}
|
||||
|
25
frontend/components/modals/pages/DeletePageModal.vue
Normal file
25
frontend/components/modals/pages/DeletePageModal.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" persistent max-width="500">
|
||||
<template #activator="{ on, attrs }">
|
||||
<slot name="activator" :attrs="attrs" :on="on" />
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('page.delete.title') }}</v-card-title>
|
||||
<v-card-text>{{ $t('page.delete.text') }}</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn text color="error" @click="dialog = false">{{ $t('general.close') }}</v-btn>
|
||||
<v-btn color="warning" @click="$emit('delete')">{{ $t('general.delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'nuxt-property-decorator';
|
||||
import { HangarModal } from '../../mixins';
|
||||
|
||||
@Component
|
||||
export default class DeletePageModal extends HangarModal {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -1,35 +1,33 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="500" persistent>
|
||||
<template #activator="{ on }">
|
||||
<v-btn icon class="primary" v-on="on"><v-icon>mdi-plus</v-icon></v-btn>
|
||||
<v-btn icon class="primary" :class="activatorClass" v-on="on"><v-icon>mdi-plus</v-icon></v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('page.new.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="pageForm" v-model="form.valid">
|
||||
<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-form>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn color="error" text @click="dialog = false">{{ $t('general.close') }}</v-btn>
|
||||
<v-btn color="success" :disabled="!form.valid" :loading="loading" @click="createPage">{{ $t('general.create') }}</v-btn>
|
||||
<v-btn color="success" :disabled="!validForm" :loading="loading" @click="createPage">{{ $t('general.create') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator';
|
||||
import { Component, Prop } from 'nuxt-property-decorator';
|
||||
import { PropType } from 'vue';
|
||||
import { HangarProjectPage } from 'hangar-internal';
|
||||
import { HangarFormModal } from '~/components/mixins';
|
||||
|
||||
@Component
|
||||
export default class NewPageModal extends Vue {
|
||||
dialog = false;
|
||||
loading = false;
|
||||
export default class NewPageModal extends HangarFormModal {
|
||||
form = {
|
||||
valid: false,
|
||||
name: '',
|
||||
parent: null as number | null,
|
||||
};
|
||||
@ -40,14 +38,6 @@ export default class NewPageModal extends Vue {
|
||||
@Prop({ type: Array as PropType<HangarProjectPage[]>, required: true })
|
||||
pages!: HangarProjectPage[];
|
||||
|
||||
@Watch('dialog')
|
||||
onToggle() {
|
||||
if (typeof this.$refs.pageForm !== 'undefined') {
|
||||
// @ts-ignore // TODO how to fix this?
|
||||
this.$refs.pageForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
createPage() {
|
||||
this.loading = true;
|
||||
this.$api
|
42
frontend/components/projects/ProjectPageList.vue
Normal file
42
frontend/components/projects/ProjectPageList.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<NewPageModal :pages="project.pages" :project-id="project.id" activator-class="mr-2" />
|
||||
{{ $t('page.plural') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-treeview :items="project.pages">
|
||||
<template #label="props">
|
||||
<v-btn v-if="!props.item.home" nuxt :to="`/${$route.params.author}/${$route.params.slug}/pages/${props.item.slug}`" color="info" text exact>
|
||||
{{ props.item.name }}
|
||||
</v-btn>
|
||||
<v-btn v-else nuxt :to="`/${$route.params.author}/${$route.params.slug}`" color="info" text exact>
|
||||
<v-icon left>mdi-home</v-icon>
|
||||
{{ props.item.name }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'nuxt-property-decorator';
|
||||
import { HangarProjectMixin } from '../mixins';
|
||||
import NewPageModal from '~/components/modals/pages/NewPageModal.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
NewPageModal,
|
||||
},
|
||||
})
|
||||
export default class ProjectPageList extends HangarProjectMixin {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'assets/utils';
|
||||
|
||||
.v-treeview a.v-btn {
|
||||
@include undoCapitalization();
|
||||
}
|
||||
</style>
|
@ -8,6 +8,7 @@ const msgs: LocaleMessageObject = {
|
||||
donate: 'Donate',
|
||||
continue: 'Continue',
|
||||
create: 'Create',
|
||||
delete: 'Delete',
|
||||
},
|
||||
hangar: {
|
||||
projectSearch: {
|
||||
@ -160,6 +161,10 @@ const msgs: LocaleMessageObject = {
|
||||
name: 'Page Name',
|
||||
parent: 'Parent Page (optional)',
|
||||
},
|
||||
delete: {
|
||||
title: 'Delete page?',
|
||||
text: 'Are you sure you want to delete this page? This cannot be undone.',
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
new: {
|
||||
|
@ -45,6 +45,7 @@
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-plugin-nuxt": "^2.0.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^7.4.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^10.5.3",
|
||||
|
@ -103,7 +103,7 @@
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-row>
|
||||
<NuxtChild class="mt-4" :project="project">
|
||||
<NuxtChild class="mt-5" :project="project">
|
||||
<v-tab-item>
|
||||
{{ $route.name }}
|
||||
</v-tab-item>
|
||||
@ -115,6 +115,7 @@
|
||||
import { Component, Vue } from 'nuxt-property-decorator';
|
||||
import { Context } from '@nuxt/types';
|
||||
import { HangarProject } from 'hangar-internal';
|
||||
import { NavigationGuardNext, Route } from 'vue-router';
|
||||
import Markdown from '~/components/Markdown.vue';
|
||||
import FlagModal from '~/components/modals/FlagModal.vue';
|
||||
import UserAvatar from '~/components/UserAvatar.vue';
|
||||
@ -144,6 +145,7 @@ 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);
|
||||
@ -218,5 +220,13 @@ export default class ProjectPage extends Vue {
|
||||
})
|
||||
.catch((err) => this.$util.handleRequestError(err, 'Could not toggle watched'));
|
||||
}
|
||||
|
||||
// Need to refresh the project if anything has changed. idk if this is the best way to do this
|
||||
async beforeRouteUpdate(to: Route, _from: Route, next: NavigationGuardNext) {
|
||||
this.project = await this.$api
|
||||
.requestInternal<HangarProject>(`projects/project/${to.params.author}/${to.params.slug}`, false)
|
||||
.catch<any>(this.$util.handlePageRequestError);
|
||||
next();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-row class="mt-5">
|
||||
<v-col v-if="!$fetchState.pending" cols="12" md="8">
|
||||
<v-row>
|
||||
<v-col v-if="page.contents" cols="12" md="8">
|
||||
<MarkdownEditor v-if="canEdit" ref="editor" :raw="page.contents" :editing.sync="editingPage" :deletable="page.deletable" @save="savePage" />
|
||||
<Markdown v-else :raw="page.contents" />
|
||||
</v-col>
|
||||
@ -35,7 +35,7 @@
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-currency-usd</v-icon>
|
||||
<v-icon left>mdi-currency-usd</v-icon>
|
||||
{{ $t('general.donate') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
@ -88,22 +88,7 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ $t('page.plural') }}
|
||||
<!-- todo new page modal -->
|
||||
<NewPageModal :pages="project.pages" :project-id="project.id" />
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<!--TODO page tree view-->
|
||||
<v-list v-if="rootPages">
|
||||
<v-list-item v-for="page in rootPages" :key="page.id"> </v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="text-center py-4">
|
||||
<v-progress-circular indeterminate />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<ProjectPageList :project="project" />
|
||||
</v-col>
|
||||
<!-- todo member list -->
|
||||
<v-col cols="12">
|
||||
@ -120,74 +105,25 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'nuxt-property-decorator';
|
||||
import { Page } from 'hangar-api';
|
||||
import { HangarProject, ProjectPage } from 'hangar-internal';
|
||||
import { PropType } from 'vue';
|
||||
import { Component } from 'nuxt-property-decorator';
|
||||
import { ProjectPage } from 'hangar-internal';
|
||||
import { Context } from '@nuxt/types';
|
||||
import MarkdownEditor from '~/components/MarkdownEditor.vue';
|
||||
import Tag from '~/components/Tag.vue';
|
||||
import DonationModal from '~/components/donation/DonationModal.vue';
|
||||
import MemberList from '~/components/MemberList.vue';
|
||||
import { NamedPermission } from '~/types/enums';
|
||||
import Markdown from '~/components/Markdown.vue';
|
||||
import NewPageModal from '~/components/modals/NewPageModal.vue';
|
||||
import NewPageModal from '~/components/modals/pages/NewPageModal.vue';
|
||||
import { DocPageMixin } from '~/components/mixins';
|
||||
import ProjectPageList from '~/components/projects/ProjectPageList.vue';
|
||||
|
||||
@Component({
|
||||
components: { NewPageModal, Markdown, MemberList, DonationModal, MarkdownEditor, Tag },
|
||||
components: { ProjectPageList, NewPageModal, Markdown, MemberList, DonationModal, MarkdownEditor, Tag },
|
||||
})
|
||||
export default class DocsPage extends Vue {
|
||||
editingPage: boolean = false;
|
||||
page = {} as ProjectPage;
|
||||
|
||||
@Prop({ type: Object as PropType<HangarProject>, required: true })
|
||||
project!: HangarProject;
|
||||
|
||||
rootPages: Array<Page> = [];
|
||||
|
||||
async fetch() {
|
||||
const page = await this.$api
|
||||
.requestInternal<ProjectPage>(`pages/page/${this.$route.params.author}/${this.$route.params.slug}`, false)
|
||||
.catch(this.$util.handleRequestError);
|
||||
this.page = page || ({} as ProjectPage);
|
||||
export default class DocsPage extends DocPageMixin {
|
||||
async asyncData({ $api, params, $util }: Context) {
|
||||
const page = await $api.requestInternal<ProjectPage>(`pages/page/${params.author}/${params.slug}`, false).catch<any>($util.handlePageRequestError);
|
||||
return { page };
|
||||
}
|
||||
|
||||
get canEdit(): boolean {
|
||||
return this.$util.hasPerms(NamedPermission.EDIT_PAGE);
|
||||
}
|
||||
|
||||
$refs!: {
|
||||
editor: MarkdownEditor;
|
||||
};
|
||||
|
||||
savePage(content: string) {
|
||||
this.$api
|
||||
.requestInternal(`pages/save/${this.project.id}/${this.page.id}`, true, 'post', {
|
||||
content,
|
||||
})
|
||||
.then(() => {
|
||||
this.page.contents = content;
|
||||
this.editingPage = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$refs.editor.loading.save = false;
|
||||
this.$util.handleRequestError(err, 'Unable to save page');
|
||||
});
|
||||
}
|
||||
|
||||
// Jake: Project categories are stored in the global store, so just get the title from there
|
||||
// formatCategory(apiName: string) {
|
||||
// const formatted = apiName.replace('_', ' ');
|
||||
// return this.capitalize(formatted);
|
||||
// }
|
||||
//
|
||||
// capitalize(input: string) {
|
||||
// return input
|
||||
// .toLowerCase()
|
||||
// .split(' ')
|
||||
// .map((s: string) => s.charAt(0).toUpperCase() + s.substring(1))
|
||||
// .join(' ');
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -1,67 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row class="mt-5">
|
||||
<v-col v-if="!$fetchState.pending && page.contents" cols="12">
|
||||
<MarkdownEditor v-if="canEdit" ref="editor" :raw="page.contents" :editing.sync="editingPage" :deletable="page.deletable" @save="savePage" />
|
||||
<v-row>
|
||||
<v-col v-if="page.contents" cols="12" md="8">
|
||||
<MarkdownEditor
|
||||
v-if="canEdit"
|
||||
ref="editor"
|
||||
:raw="page.contents"
|
||||
:editing.sync="editingPage"
|
||||
:deletable="page.deletable"
|
||||
@save="savePage"
|
||||
@delete="deletePage"
|
||||
/>
|
||||
<Markdown v-else :raw="page.contents" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<ProjectPageList :project="project" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'nuxt-property-decorator';
|
||||
import { HangarProject, ProjectPage } from 'hangar-internal';
|
||||
import { PropType } from 'vue';
|
||||
import { NamedPermission } from '~/types/enums';
|
||||
import { Component } from 'nuxt-property-decorator';
|
||||
import { ProjectPage } from 'hangar-internal';
|
||||
import { Context } from '@nuxt/types';
|
||||
import MarkdownEditor from '~/components/MarkdownEditor.vue';
|
||||
import Markdown from '~/components/Markdown.vue';
|
||||
import { DocPageMixin } from '~/components/mixins';
|
||||
import ProjectPageList from '~/components/projects/ProjectPageList.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ProjectPageList,
|
||||
MarkdownEditor,
|
||||
Markdown,
|
||||
},
|
||||
})
|
||||
export default class VueProjectPage extends Vue {
|
||||
editingPage: boolean = false;
|
||||
page = {
|
||||
contents: '',
|
||||
deletable: false,
|
||||
} as ProjectPage;
|
||||
|
||||
@Prop({ type: Object as PropType<HangarProject>, required: true })
|
||||
project!: HangarProject;
|
||||
|
||||
async fetch() {
|
||||
this.page = await this.$api
|
||||
.requestInternal<ProjectPage>(`pages/page/${this.$route.params.author}/${this.$route.params.slug}/${this.$route.params.pathMatch}`, false)
|
||||
.catch<any>(this.$util.handlePageRequestError);
|
||||
}
|
||||
|
||||
get canEdit(): boolean {
|
||||
return this.$util.hasPerms(NamedPermission.EDIT_PAGE);
|
||||
}
|
||||
|
||||
$refs!: {
|
||||
editor: MarkdownEditor;
|
||||
};
|
||||
|
||||
savePage(content: string) {
|
||||
this.$api
|
||||
.requestInternal(`pages/save/${this.project.id}/${this.page.id}`, true, 'post', {
|
||||
content,
|
||||
})
|
||||
.then(() => {
|
||||
this.page.contents = content;
|
||||
this.editingPage = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$refs.editor.loading.save = false;
|
||||
this.$util.handleRequestError(err, 'Unable to save page');
|
||||
export default class VueProjectPage extends DocPageMixin {
|
||||
async asyncData({ $api, params, $util, beforeNuxtRender }: Context) {
|
||||
if (process.server) {
|
||||
beforeNuxtRender(({ nuxtState }) => {
|
||||
console.log(nuxtState);
|
||||
});
|
||||
}
|
||||
const page = await $api
|
||||
.requestInternal<ProjectPage>(`pages/page/${params.author}/${params.slug}/${params.pathMatch}`, false)
|
||||
.catch<any>($util.handlePageRequestError);
|
||||
return { page };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -131,6 +131,7 @@ 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;
|
||||
store.dispatch('snackbar/SHOW_NOTIF', {
|
||||
|
1
frontend/types/internal/projects.d.ts
vendored
1
frontend/types/internal/projects.d.ts
vendored
@ -20,6 +20,7 @@ declare module 'hangar-internal' {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
home: boolean;
|
||||
children: HangarProjectPage[];
|
||||
}
|
||||
|
||||
|
@ -4181,6 +4181,13 @@ eslint-plugin-nuxt@^2.0.0:
|
||||
semver "^7.3.2"
|
||||
vue-eslint-parser "^7.1.1"
|
||||
|
||||
eslint-plugin-prettier@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7"
|
||||
integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
eslint-plugin-promise@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
|
||||
@ -4580,6 +4587,11 @@ fast-deep-equal@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||
|
||||
fast-glob@^3.1.1:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
|
||||
@ -8023,6 +8035,13 @@ prepend-http@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^1.18.2:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
|
@ -59,7 +59,7 @@ public class WebConfig extends WebMvcConfigurationSupport {
|
||||
|
||||
@Override
|
||||
protected void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/internal/**").allowedOrigins(hangarConfig.isUseWebpack() ? "http://localhost:3000" : "https://hangar.minidigger.me");
|
||||
registry.addMapping("/api/internal/**").allowedOrigins(hangarConfig.isDev() ? "http://localhost:3000" : "https://hangar.minidigger.me");
|
||||
}
|
||||
|
||||
// TODO remove after freemarker is gone
|
||||
|
@ -28,7 +28,6 @@ public class ApiConfig {
|
||||
private Duration publicExpiration = Duration.ofHours(3);
|
||||
@DurationUnit(ChronoUnit.DAYS)
|
||||
private Duration expiration = Duration.ofDays(14);
|
||||
private String checkInterval = "5m";
|
||||
|
||||
|
||||
public Duration getPublicExpiration() {
|
||||
@ -46,13 +45,5 @@ public class ApiConfig {
|
||||
public void setExpiration(Duration expiration) {
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
public String getCheckInterval() {
|
||||
return checkInterval;
|
||||
}
|
||||
|
||||
public void setCheckInterval(String checkInterval) {
|
||||
this.checkInterval = checkInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,14 +23,11 @@ public class HangarConfig {
|
||||
private String service = "Hangar";
|
||||
private List<Sponsor> sponsors;
|
||||
|
||||
private boolean useWebpack = false;
|
||||
private boolean debug = false;
|
||||
private int debugLevel = 3;
|
||||
private boolean logTimings = false;
|
||||
private String authUrl = "https://hangarauth.minidigger.me";
|
||||
private boolean dev = true;
|
||||
private String authUrl;
|
||||
private final ApplicationHome home = new ApplicationHome(HangarApplication.class);
|
||||
private String pluginUploadDir = home.getDir().toPath().resolve("work").toString();
|
||||
private String baseUrl = "https://localhost:8080";
|
||||
private String baseUrl;
|
||||
private String gaCode = "";
|
||||
private List<Announcement> announcements = new ArrayList<>();
|
||||
|
||||
@ -56,8 +53,6 @@ public class HangarConfig {
|
||||
public HangarSecurityConfig security;
|
||||
@NestedConfigurationProperty
|
||||
public QueueConfig queue;
|
||||
@NestedConfigurationProperty
|
||||
public SessionConfig session;
|
||||
|
||||
@Component
|
||||
public static class Sponsor {
|
||||
@ -91,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, SessionConfig session) {
|
||||
public HangarConfig(FakeUserConfig fakeUser, HomepageConfig homepage, ChannelsConfig channels, PagesConfig pages, ProjectsConfig projects, UserConfig user, OrgConfig org, ApiConfig api, SsoConfig sso, HangarSecurityConfig security, QueueConfig queue) {
|
||||
this.fakeUser = fakeUser;
|
||||
this.homepage = homepage;
|
||||
this.channels = channels;
|
||||
@ -103,7 +98,12 @@ public class HangarConfig {
|
||||
this.sso = sso;
|
||||
this.security = security;
|
||||
this.queue = queue;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void checkDev() {
|
||||
if (!this.dev) {
|
||||
throw new UnsupportedOperationException("Only supported in dev mode!");
|
||||
}
|
||||
}
|
||||
|
||||
public String getLogo() {
|
||||
@ -130,30 +130,6 @@ public class HangarConfig {
|
||||
this.sponsors = sponsors;
|
||||
}
|
||||
|
||||
public boolean isUseWebpack() {
|
||||
return useWebpack;
|
||||
}
|
||||
|
||||
public void setUseWebpack(boolean useWebpack) {
|
||||
this.useWebpack = useWebpack;
|
||||
}
|
||||
|
||||
public boolean isDebug() {
|
||||
return debug;
|
||||
}
|
||||
|
||||
public void setDebug(boolean debug) {
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
public int getDebugLevel() {
|
||||
return debugLevel;
|
||||
}
|
||||
|
||||
public void setDebugLevel(int debugLevel) {
|
||||
this.debugLevel = debugLevel;
|
||||
}
|
||||
|
||||
public List<Announcement> getAnnouncements() {
|
||||
return announcements;
|
||||
}
|
||||
@ -162,12 +138,12 @@ public class HangarConfig {
|
||||
this.announcements = announcements;
|
||||
}
|
||||
|
||||
public boolean isLogTimings() {
|
||||
return logTimings;
|
||||
public boolean isDev() {
|
||||
return dev;
|
||||
}
|
||||
|
||||
public void setLogTimings(boolean logTimings) {
|
||||
this.logTimings = logTimings;
|
||||
public void setDev(boolean dev) {
|
||||
this.dev = dev;
|
||||
}
|
||||
|
||||
public String getAuthUrl() {
|
||||
@ -202,12 +178,6 @@ public class HangarConfig {
|
||||
this.gaCode = gaCode;
|
||||
}
|
||||
|
||||
public void checkDebug() {
|
||||
if (!debug) {
|
||||
throw new UnsupportedOperationException("this function is supported in debug mode only");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValidProjectName(String name) {
|
||||
String sanitized = StringUtils.compact(name);
|
||||
return sanitized.length() >= 1 && sanitized.length() <= projects.getMaxNameLen() && projects.getNameMatcher().test(name);
|
||||
@ -257,8 +227,4 @@ public class HangarConfig {
|
||||
public QueueConfig getQueue() {
|
||||
return queue;
|
||||
}
|
||||
|
||||
public SessionConfig getSession() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
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;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -23,6 +25,7 @@ public class ProjectsConfig {
|
||||
private int maxDescLen = 120;
|
||||
private int maxKeywords = 5;
|
||||
private boolean fileValidate = true;
|
||||
@DurationUnit(ChronoUnit.DAYS)
|
||||
private Duration staleAge = Duration.ofDays(28);
|
||||
private String checkInterval = "1h";
|
||||
private String draftExpire = "1d";
|
||||
|
@ -1,30 +0,0 @@
|
||||
package io.papermc.hangar.config.hangar;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "session")
|
||||
public class SessionConfig {
|
||||
private boolean secure = false;
|
||||
private Duration maxAge = Duration.of(28, ChronoUnit.DAYS);
|
||||
|
||||
public boolean isSecure() {
|
||||
return secure;
|
||||
}
|
||||
|
||||
public void setSecure(boolean secure) {
|
||||
this.secure = secure;
|
||||
}
|
||||
|
||||
public Duration getMaxAge() {
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
public void setMaxAge(Duration maxAge) {
|
||||
this.maxAge = maxAge;
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import java.time.Duration;
|
||||
@ConfigurationProperties(prefix = "hangar.sso")
|
||||
public class SsoConfig {
|
||||
|
||||
// TODO weed out the useless settings
|
||||
private boolean enabled = true;
|
||||
private String loginUrl = "/sso/";
|
||||
private String signupUrl = "/sso/signup/";
|
||||
|
@ -8,20 +8,11 @@ import java.util.List;
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "hangar.users")
|
||||
public class UserConfig {
|
||||
private int starsPerPage = 5;
|
||||
private int maxTaglineLen = 100;
|
||||
@Deprecated
|
||||
private int authorPageSize = 25;
|
||||
private int projectPageSize = 5;
|
||||
private List<String> staffRoles = List.of("Hangar_Admin", "Hangar_Mod");
|
||||
|
||||
public int getStarsPerPage() {
|
||||
return starsPerPage;
|
||||
}
|
||||
|
||||
public void setStarsPerPage(int starsPerPage) {
|
||||
this.starsPerPage = starsPerPage;
|
||||
}
|
||||
|
||||
public int getMaxTaglineLen() {
|
||||
return maxTaglineLen;
|
||||
}
|
||||
@ -38,14 +29,6 @@ public class UserConfig {
|
||||
this.authorPageSize = authorPageSize;
|
||||
}
|
||||
|
||||
public int getProjectPageSize() {
|
||||
return projectPageSize;
|
||||
}
|
||||
|
||||
public void setProjectPageSize(int projectPageSize) {
|
||||
this.projectPageSize = projectPageSize;
|
||||
}
|
||||
|
||||
public List<String> getStaffRoles() {
|
||||
return staffRoles;
|
||||
}
|
||||
|
@ -4,12 +4,9 @@ import io.papermc.hangar.controller.HangarController;
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.IAuthenticationController;
|
||||
import io.papermc.hangar.model.api.auth.ApiSession;
|
||||
import io.papermc.hangar.model.api.requests.SessionProperties;
|
||||
import io.papermc.hangar.security.HangarAuthenticationToken;
|
||||
import io.papermc.hangar.service.AuthenticationService;
|
||||
import io.papermc.hangar.util.AuthUtils;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
@ -21,22 +18,9 @@ public class AuthenticationController extends HangarController implements IAuthe
|
||||
this.authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
// TODO JWT
|
||||
@Override
|
||||
public ResponseEntity<ApiSession> authenticate(SessionProperties body) {
|
||||
if (body != null && body.isFake()) {
|
||||
return ResponseEntity.ok(authenticationService.authenticateDev());
|
||||
} else {
|
||||
return ResponseEntity.ok(authenticationService.authenticateKeyPublic(body == null ? null : body.getExpiresIn()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<ApiSession> authenticateUser(SessionProperties body) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication instanceof HangarAuthenticationToken) {
|
||||
return ResponseEntity.ok(authenticationService.authenticateUser(((HangarAuthenticationToken) authentication).getUserId()));
|
||||
} else {
|
||||
throw AuthUtils.unAuth();
|
||||
}
|
||||
throw new NotImplementedException("Setup JWT here");
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
package io.papermc.hangar.controller.api.v1;
|
||||
|
||||
import io.papermc.hangar.controller.HangarController;
|
||||
import io.papermc.hangar.controller.api.v1.interfaces.ISessionsController;
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.ApiSessionDAO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
public class SessionController extends HangarController implements ISessionsController {
|
||||
|
||||
private final ApiSessionDAO apiSessionDAO;
|
||||
|
||||
@Autowired
|
||||
public SessionController(HangarDao<ApiSessionDAO> apiSessionDAO) {
|
||||
this.apiSessionDAO = apiSessionDAO.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> deleteSession() {
|
||||
// apiSessionDAO.delete(hangarApiRequest.getSession());
|
||||
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
}
|
@ -33,8 +33,4 @@ public interface IAuthenticationController {
|
||||
})
|
||||
@PostMapping("/authenticate")
|
||||
ResponseEntity<ApiSession> authenticate(@ApiParam("Session properties") @RequestBody(required = false) SessionProperties body);
|
||||
|
||||
@ApiOperation(value = "authenticateUser", hidden = true)
|
||||
@PostMapping("/authenticate/user")
|
||||
ResponseEntity<ApiSession> authenticateUser(@ApiParam @RequestBody(required = false) SessionProperties body);
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
package io.papermc.hangar.controller.api.v1.interfaces;
|
||||
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
import io.swagger.annotations.Authorization;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
@Api(tags = "Sessions (Authentication)")
|
||||
@RequestMapping(path = "/api/v1", method = RequestMethod.DELETE)
|
||||
public interface ISessionsController {
|
||||
|
||||
@ApiOperation(
|
||||
value = "Invalidates the API session used for the request.",
|
||||
nickname = "deleteSession",
|
||||
notes = "Invalidates the API session used to make this call.",
|
||||
authorizations = @Authorization(value = "Session"),
|
||||
tags = "Sessions (Authentication)")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(code = 204, message = "Session invalidated"),
|
||||
@ApiResponse(code = 400, message = "Sent if this request was not made with a session."),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@DeleteMapping("/sessions/current")
|
||||
@PreAuthorize("@authenticationService.handleApiRequest(T(io.papermc.hangar.model.common.Permission).None, T(io.papermc.hangar.controller.extras.ApiScope).ofGlobal())")
|
||||
ResponseEntity<Void> deleteSession();
|
||||
}
|
@ -28,6 +28,7 @@ public class ContentSecurityPolicyFilter extends OncePerRequestFilter {
|
||||
|
||||
private final String cspHeader;
|
||||
|
||||
// TODO update for new frontend (probably dont even want this anymore)
|
||||
@Autowired
|
||||
public ContentSecurityPolicyFilter(HangarConfig hangarConfig) {
|
||||
this.hangarConfig = hangarConfig;
|
||||
@ -48,7 +49,7 @@ public class ContentSecurityPolicyFilter extends OncePerRequestFilter {
|
||||
.base_uri(CSP.NONE)
|
||||
.block_all_mixed_content();
|
||||
|
||||
if (hangarConfig.isUseWebpack()) {
|
||||
if (hangarConfig.isDev()) {
|
||||
String webpack = "http://localhost:8081";
|
||||
String webSocket = "ws://*:8081";
|
||||
String socketIo = "http://*:8081";
|
||||
|
@ -51,7 +51,7 @@ public class LoginController extends HangarController {
|
||||
@GetMapping(path = "/login", params = "returnUrl")
|
||||
public Object loginFromFrontend(@RequestParam(defaultValue = Routes.Paths.SHOW_HOME) String returnUrl, RedirectAttributes attributes) {
|
||||
if (hangarConfig.fakeUser.isEnabled()) {
|
||||
hangarConfig.checkDebug();
|
||||
hangarConfig.checkDev();
|
||||
|
||||
UserTable fakeUser = authenticationService.loginAsFakeUser();
|
||||
tokenService.createTokenForUser(fakeUser);
|
||||
@ -78,7 +78,6 @@ public class LoginController extends HangarController {
|
||||
UserTable user = userService.getOrCreate(authUser.getUserName(), authUser);
|
||||
globalRoleService.removeAllGlobalRoles(user.getId());
|
||||
authUser.getGlobalRoles().forEach(globalRole -> globalRoleService.addRole(globalRole.create(null, user.getId(), true)));
|
||||
authenticationService.setAuthenticatedUser(user);
|
||||
String token = tokenService.createTokenForUser(user);
|
||||
return redirectBackOnSuccessfulLogin(url + "?token=" + token, user);
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@ -71,7 +70,7 @@ public class ProjectPageController extends HangarController {
|
||||
|
||||
@Unlocked
|
||||
@PermissionRequired(perms = NamedPermission.EDIT_PAGE, type = PermissionType.PROJECT, args = "{#projectId}")
|
||||
@DeleteMapping("/delete/{projectId}/{pageId}")
|
||||
@PostMapping("/delete/{projectId}/{pageId}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void deleteProjectPage(@PathVariable long projectId, @PathVariable long pageId) {
|
||||
projectPageService.deleteProjectPage(projectId, pageId);
|
||||
|
@ -402,7 +402,7 @@ public class ApplicationController extends HangarController {
|
||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseBody
|
||||
public Object robots() {
|
||||
if (hangarConfig.isUseWebpack()) {
|
||||
if (hangarConfig.isDev()) {
|
||||
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, HttpStatus.MOVED_PERMANENTLY);
|
||||
return new ModelAndView("redirect:http://localhost:8081/robots.txt");
|
||||
} else {
|
||||
|
@ -1,22 +0,0 @@
|
||||
package io.papermc.hangar.db.dao.internal.table.auth;
|
||||
|
||||
import io.papermc.hangar.model.db.auth.ApiSessionTable;
|
||||
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.SqlUpdate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@RegisterConstructorMapper(ApiSessionTable.class)
|
||||
public interface ApiSessionDAO {
|
||||
|
||||
@Timestamped
|
||||
@GetGeneratedKeys
|
||||
@SqlUpdate("INSERT INTO api_sessions (created_at, token, key_id, user_id, expires) VALUES (:now, :token, :keyId, :userId, :expires)")
|
||||
ApiSessionTable insert(@BindBean ApiSessionTable apiSessionTable);
|
||||
|
||||
@SqlUpdate("DELETE FROM api_sessions WHERE token = :token")
|
||||
void delete(String token);
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package io.papermc.hangar.model.db.auth;
|
||||
|
||||
import io.papermc.hangar.model.db.Table;
|
||||
import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public class ApiSessionTable extends Table {
|
||||
|
||||
private final String token;
|
||||
private final Long keyId;
|
||||
private final Long userId;
|
||||
private final OffsetDateTime expires;
|
||||
|
||||
@JdbiConstructor
|
||||
public ApiSessionTable(OffsetDateTime createdAt, long id, String token, Long keyId, Long userId, OffsetDateTime expires) {
|
||||
super(createdAt, id);
|
||||
this.token = token;
|
||||
this.keyId = keyId;
|
||||
this.userId = userId;
|
||||
this.expires = expires;
|
||||
}
|
||||
|
||||
private ApiSessionTable(String token, Long keyId, Long userId, OffsetDateTime expires) {
|
||||
this.token = token;
|
||||
this.keyId = keyId;
|
||||
this.userId = userId;
|
||||
this.expires = expires;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public Long getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public OffsetDateTime getExpires() {
|
||||
return expires;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ApiSessionTable{" +
|
||||
"token='" + token + '\'' +
|
||||
", keyId=" + keyId +
|
||||
", userId=" + userId +
|
||||
", expires=" + expires +
|
||||
"} " + super.toString();
|
||||
}
|
||||
|
||||
public static ApiSessionTable fromApiKey(String token, ApiKeyTable apiKeyTable, OffsetDateTime expires) {
|
||||
return new ApiSessionTable(token, apiKeyTable.getId(), apiKeyTable.getOwnerId(), expires);
|
||||
}
|
||||
|
||||
public static ApiSessionTable createPublicKey(String token, OffsetDateTime expires) {
|
||||
return new ApiSessionTable(token, null, null, expires);
|
||||
}
|
||||
|
||||
public static ApiSessionTable createUserKey(String token, long userId, OffsetDateTime expires) {
|
||||
return new ApiSessionTable(token, null, userId, expires);
|
||||
}
|
||||
}
|
@ -13,12 +13,14 @@ public class HangarProjectPage {
|
||||
private final long id;
|
||||
private final String name;
|
||||
private final String slug;
|
||||
private final boolean isHome;
|
||||
private final Map<Long, HangarProjectPage> children;
|
||||
|
||||
public HangarProjectPage(ProjectPageTable projectPageTable) {
|
||||
public HangarProjectPage(ProjectPageTable projectPageTable, boolean isHome) {
|
||||
this.id = projectPageTable.getId();
|
||||
this.name = projectPageTable.getName();
|
||||
this.slug = projectPageTable.getSlug();
|
||||
this.isHome = isHome;
|
||||
this.children = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
@ -34,6 +36,10 @@ public class HangarProjectPage {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public boolean isHome() {
|
||||
return isHome;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Map<Long, HangarProjectPage> getChildren() {
|
||||
return children;
|
||||
|
@ -1,206 +1,23 @@
|
||||
package io.papermc.hangar.service;
|
||||
|
||||
import io.papermc.hangar.config.hangar.HangarConfig;
|
||||
import io.papermc.hangar.controller.extras.ApiScope;
|
||||
import io.papermc.hangar.controller.extras.HangarApiRequest;
|
||||
import io.papermc.hangar.controller.extras.HangarRequest;
|
||||
import io.papermc.hangar.db.dao.HangarDao;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.ApiKeyDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.ApiSessionDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.projects.ProjectsDAO;
|
||||
import io.papermc.hangar.db.dao.session.HangarRequestDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.auth.ApiSession;
|
||||
import io.papermc.hangar.model.common.Permission;
|
||||
import io.papermc.hangar.model.common.roles.GlobalRole;
|
||||
import io.papermc.hangar.model.db.OrganizationTable;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.db.auth.ApiKeyTable;
|
||||
import io.papermc.hangar.model.db.auth.ApiSessionTable;
|
||||
import io.papermc.hangar.model.db.projects.ProjectTable;
|
||||
import io.papermc.hangar.security.HangarAuthenticationToken;
|
||||
import io.papermc.hangar.service.internal.OrganizationService;
|
||||
import io.papermc.hangar.service.internal.UserService;
|
||||
import io.papermc.hangar.service.internal.roles.GlobalRoleService;
|
||||
import io.papermc.hangar.util.AuthUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.annotation.RequestScope;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class AuthenticationService extends HangarService {
|
||||
|
||||
private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
||||
private static final Pattern API_KEY_PATTERN = Pattern.compile("(" + UUID_REGEX + ").(" + UUID_REGEX + ")");
|
||||
|
||||
private final HangarConfig hangarConfig;
|
||||
private final HangarRequestDAO hangarRequestDAO;
|
||||
private final ApiSessionDAO apiSessionDAO;
|
||||
private final ProjectsDAO projectsDAO;
|
||||
private final ApiKeyDAO apiKeyDAO;
|
||||
private final PermissionService permissionService;
|
||||
private final VisibilityService visibilityService;
|
||||
private final OrganizationService organizationService;
|
||||
private final UserService userService;
|
||||
private final GlobalRoleService globalRoleService;
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
public AuthenticationService(HangarConfig hangarConfig, HangarDao<HangarRequestDAO> hangarRequestDAO, HangarDao<ApiSessionDAO> apiSessionDAO, HangarDao<ProjectsDAO> projectDAO, HangarDao<ApiKeyDAO> apiKeyDAO, PermissionService permissionService, VisibilityService visibilityService, OrganizationService organizationService, UserService userService, GlobalRoleService globalRoleService, HttpServletRequest request) {
|
||||
this.hangarConfig = hangarConfig;
|
||||
this.hangarRequestDAO = hangarRequestDAO.get();
|
||||
this.apiSessionDAO = apiSessionDAO.get();
|
||||
this.projectsDAO = projectDAO.get();
|
||||
this.apiKeyDAO = apiKeyDAO.get();
|
||||
this.permissionService = permissionService;
|
||||
this.visibilityService = visibilityService;
|
||||
this.organizationService = organizationService;
|
||||
public AuthenticationService(UserService userService, GlobalRoleService globalRoleService) {
|
||||
this.userService = userService;
|
||||
this.globalRoleService = globalRoleService;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@RequestScope
|
||||
public HangarApiRequest hangarApiRequest() {
|
||||
AuthUtils.AuthCredentials credentials = AuthUtils.parseAuthHeader(request, true);
|
||||
if (credentials.getSession() == null) {
|
||||
throw AuthUtils.unAuth("No session specified");
|
||||
}
|
||||
HangarApiRequest hangarApiRequest = hangarRequestDAO.createHangarRequest(credentials.getSession());
|
||||
if (hangarApiRequest == null) {
|
||||
throw AuthUtils.unAuth("Invalid session");
|
||||
}
|
||||
if (hangarApiRequest.getExpires().isBefore(OffsetDateTime.now())) {
|
||||
apiSessionDAO.delete(credentials.getSession());
|
||||
throw AuthUtils.unAuth("Api session expired");
|
||||
}
|
||||
return hangarApiRequest;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@RequestScope
|
||||
public HangarRequest hangarRequest() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
UserTable userTable = null;
|
||||
Long userId = null;
|
||||
if (authentication instanceof HangarAuthenticationToken) {
|
||||
HangarAuthenticationToken hangarAuthentication = (HangarAuthenticationToken) authentication;
|
||||
userTable = userService.getUserTable(hangarAuthentication.getUserId());
|
||||
if (userTable != null) userId = userTable.getUserId();
|
||||
}
|
||||
return new HangarRequest(userTable, permissionService.getGlobalPermissions(userId));
|
||||
}
|
||||
|
||||
private boolean checkPerms(Permission requiredPerms, ApiScope apiScope, Long userId) {
|
||||
switch (apiScope.getType()) {
|
||||
case GLOBAL:
|
||||
return permissionService.getGlobalPermissions(userId).has(requiredPerms);
|
||||
case PROJECT:
|
||||
if ((StringUtils.isEmpty(apiScope.getOwner()) || StringUtils.isEmpty(apiScope.getSlug())) && apiScope.getId() == null) {
|
||||
throw new IllegalArgumentException("Must have passed an (owner and slug) OR an ID to apiAction");
|
||||
}
|
||||
ProjectTable projectTable;
|
||||
Permission projectPermissions;
|
||||
if (apiScope.getId() != null) {
|
||||
projectPermissions = permissionService.getProjectPermissions(userId, apiScope.getId());
|
||||
projectTable = visibilityService.checkVisibility(projectsDAO.getById(apiScope.getId()), projectPermissions);
|
||||
}
|
||||
else {
|
||||
projectPermissions = permissionService.getProjectPermissions(userId, apiScope.getOwner(), apiScope.getSlug());
|
||||
projectTable = visibilityService.checkVisibility(projectsDAO.getBySlug(apiScope.getOwner(), apiScope.getSlug()), projectPermissions);
|
||||
}
|
||||
if (projectTable == null) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND, "Resource NOT FOUND");
|
||||
}
|
||||
return projectPermissions.has(requiredPerms);
|
||||
case ORGANIZATION:
|
||||
if (StringUtils.isEmpty(apiScope.getOwner())) {
|
||||
throw new IllegalArgumentException("Must have passed the owner to apiAction");
|
||||
}
|
||||
OrganizationTable organizationTable = organizationService.getOrganizationTable(apiScope.getOwner());
|
||||
if (organizationTable == null) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return permissionService.getOrganizationPermissions(userId, apiScope.getOwner()).has(requiredPerms);
|
||||
default:
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
public ApiSession authenticateDev() {
|
||||
if (hangarConfig.fakeUser.isEnabled()) {
|
||||
hangarConfig.checkDebug();
|
||||
OffsetDateTime sessionExpiration = AuthUtils.expiration(hangarConfig.api.session.getExpiration(), null);
|
||||
String uuidToken = UUID.randomUUID().toString();
|
||||
|
||||
ApiSessionTable apiSessionTable = ApiSessionTable.createUserKey(uuidToken, hangarConfig.fakeUser.getId(), sessionExpiration);
|
||||
saveApiSessionTable(apiSessionTable);
|
||||
|
||||
return new ApiSession(apiSessionTable.getToken(), apiSessionTable.getExpires(), ApiSession.SessionType.DEV);
|
||||
} else {
|
||||
throw new HangarApiException(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
public ApiSession authenticateKeyPublic(Long expiresIn) {
|
||||
OffsetDateTime sessionExpiration = AuthUtils.expiration(hangarConfig.api.session.getExpiration(), expiresIn);
|
||||
OffsetDateTime publicSessionExpiration = AuthUtils.expiration(hangarConfig.api.session.getPublicExpiration(), expiresIn);
|
||||
String uuidToken = UUID.randomUUID().toString();
|
||||
|
||||
AuthUtils.AuthCredentials credentials = AuthUtils.parseAuthHeader(false);
|
||||
ApiSession.SessionType sessionType;
|
||||
ApiSessionTable apiSession;
|
||||
if (credentials.getApiKey() != null) {
|
||||
if (!API_KEY_PATTERN.matcher(credentials.getApiKey()).find()) {
|
||||
throw AuthUtils.unAuth("No valid apikey parameter found in Authorization");
|
||||
}
|
||||
if (sessionExpiration == null) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "The requested expiration can't be used");
|
||||
}
|
||||
// I THINK that is how its setup, couldn't really figure it out via ore
|
||||
String identifier = credentials.getApiKey().split("\\.")[0];
|
||||
String token = credentials.getApiKey().split("\\.")[1];
|
||||
ApiKeyTable apiKeysTable = apiKeyDAO.findApiKey(identifier, token);
|
||||
if (apiKeysTable == null) {
|
||||
throw AuthUtils.unAuth("Invalid api key");
|
||||
}
|
||||
apiSession = ApiSessionTable.fromApiKey(uuidToken, apiKeysTable, sessionExpiration);
|
||||
sessionType = ApiSession.SessionType.KEY;
|
||||
} else {
|
||||
if (publicSessionExpiration == null) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "The requested expiration can't be used");
|
||||
}
|
||||
apiSession = ApiSessionTable.createPublicKey(uuidToken, publicSessionExpiration);
|
||||
sessionType = ApiSession.SessionType.PUBLIC;
|
||||
}
|
||||
|
||||
saveApiSessionTable(apiSession);
|
||||
return new ApiSession(apiSession.getToken(), apiSession.getExpires(), sessionType);
|
||||
}
|
||||
|
||||
public ApiSession authenticateUser(long userId) {
|
||||
OffsetDateTime sessionExpiration = AuthUtils.expiration(hangarConfig.api.session.getExpiration(), null);
|
||||
String uuidToken = UUID.randomUUID().toString();
|
||||
|
||||
ApiSessionTable apiSession = ApiSessionTable.createUserKey(uuidToken, userId, sessionExpiration);
|
||||
saveApiSessionTable(apiSession);
|
||||
return new ApiSession(apiSession.getToken(), apiSession.getExpires(), ApiSession.SessionType.USER);
|
||||
}
|
||||
|
||||
private void saveApiSessionTable(ApiSessionTable apiSessionTable) {
|
||||
apiSessionDAO.insert(apiSessionTable);
|
||||
}
|
||||
|
||||
public UserTable loginAsFakeUser() {
|
||||
@ -221,14 +38,6 @@ public class AuthenticationService extends HangarService {
|
||||
|
||||
globalRoleService.addRole(GlobalRole.HANGAR_ADMIN.create(null, userTable.getId(), true));
|
||||
}
|
||||
setAuthenticatedUser(userTable);
|
||||
return userTable;
|
||||
}
|
||||
|
||||
public void setAuthenticatedUser(UserTable user) {
|
||||
// TODO properly do auth, remember me shit too
|
||||
// Authentication auth = new HangarAuthenticationToken(List.of(new SimpleGrantedAuthority("ROLE_USER")), user.getName(), user.getId());
|
||||
// authenticationManager.authenticate(auth);
|
||||
// SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ public class TokenService extends HangarService {
|
||||
|
||||
public String createTokenForUser(UserTable userTable) {
|
||||
UserRefreshToken userRefreshToken = userRefreshTokenDAO.insert(new UserRefreshToken(userTable.getId(), UUID.randomUUID(), UUID.randomUUID()));
|
||||
response.addCookie(CookieUtils.builder(SecurityConfig.AUTH_NAME_REFRESH_COOKIE, userRefreshToken.getToken().toString()).withComment("Refresh token for a JWT").setPath("/").setSecure(!hangarConfig.isUseWebpack()).setMaxAge((int) hangarConfig.security.getRefreshTokenExpiry().toSeconds()).build());
|
||||
response.addCookie(CookieUtils.builder(SecurityConfig.AUTH_NAME_REFRESH_COOKIE, userRefreshToken.getToken().toString()).withComment("Refresh token for a JWT").setPath("/").setSecure(hangarConfig.security.isSecure()).setMaxAge((int) hangarConfig.security.getRefreshTokenExpiry().toSeconds()).build());
|
||||
return _newToken(userTable, userRefreshToken);
|
||||
}
|
||||
|
||||
|
@ -27,12 +27,6 @@ public class ProjectPageService extends HangarService {
|
||||
}
|
||||
|
||||
public ProjectPageTable createPage(long projectId, String name, String slug, String contents, boolean deletable, @Nullable Long parentId, boolean isHome) {
|
||||
// TODO below code was for ensuring no more than 1 level of nesting
|
||||
// if (parentId != null) {
|
||||
// if (!projectPagesDAO.getRootPages(projectId).containsKey(parentId)) {
|
||||
// throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||
// }
|
||||
// }
|
||||
|
||||
if ((!isHome && name.equalsIgnoreCase(hangarConfig.pages.home.getName())) && contents.length() < hangarConfig.pages.getMinLen()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.minLength");
|
||||
@ -54,20 +48,22 @@ public class ProjectPageService extends HangarService {
|
||||
deletable,
|
||||
parentId
|
||||
);
|
||||
return projectPagesDAO.insert(projectPageTable);
|
||||
projectPageTable = projectPagesDAO.insert(projectPageTable);
|
||||
userActionLogService.projectPage(LoggedActionType.PROJECT_PAGE_CREATED.with(ProjectPageContext.of(projectPageTable.getProjectId(), projectPageTable.getId())), contents, "");
|
||||
return projectPageTable;
|
||||
}
|
||||
|
||||
public Map<Long, HangarProjectPage> getProjectPages(long projectId) {
|
||||
Map<Long, HangarProjectPage> hangarProjectPages = new LinkedHashMap<>();
|
||||
for (ProjectPageTable projectPage : projectPagesDAO.getProjectPages(projectId)) {
|
||||
if (projectPage.getParentId() == null) {
|
||||
hangarProjectPages.put(projectPage.getId(), new HangarProjectPage(projectPage));
|
||||
hangarProjectPages.put(projectPage.getId(), new HangarProjectPage(projectPage, projectPage.getName().equals(hangarConfig.pages.home.getName())));
|
||||
} else {
|
||||
HangarProjectPage parent = findById(projectPage.getParentId(), hangarProjectPages);
|
||||
if (parent == null) {
|
||||
throw new IllegalStateException("Should always find a parent");
|
||||
}
|
||||
parent.getChildren().put(projectPage.getId(), new HangarProjectPage(projectPage));
|
||||
parent.getChildren().put(projectPage.getId(), new HangarProjectPage(projectPage, projectPage.getName().equals(hangarConfig.pages.home.getName())));
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,7 +126,8 @@ public class ProjectPageService extends HangarService {
|
||||
if (!pageTable.isDeletable()) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "Page is not deletable");
|
||||
}
|
||||
projectPagesDAO.delete(pageTable);
|
||||
// Log must come first otherwise db error
|
||||
userActionLogService.projectPage(LoggedActionType.PROJECT_PAGE_DELETED.with(ProjectPageContext.of(projectId, pageId)), "", pageTable.getContents());
|
||||
projectPagesDAO.delete(pageTable);
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ public class AuthenticationService extends HangarService {
|
||||
|
||||
public ApiSessionResponse authenticateDev() {
|
||||
if (hangarConfig.fakeUser.isEnabled()) {
|
||||
hangarConfig.checkDebug();
|
||||
hangarConfig.checkDev();
|
||||
OffsetDateTime sessionExpiration = AuthUtils.expiration(hangarConfig.api.session.getExpiration(), null);
|
||||
String uuidToken = UUID.randomUUID().toString();
|
||||
|
||||
|
@ -42,9 +42,7 @@ fake-user:
|
||||
|
||||
|
||||
hangar:
|
||||
debug: true
|
||||
debug-level: 3
|
||||
log-timings: false
|
||||
dev: true
|
||||
auth-url: "http://localhost:8000"
|
||||
base-url: "http://localhost:8080"
|
||||
use-webpack: true
|
||||
@ -101,10 +99,8 @@ hangar:
|
||||
name-regex: "^[a-zA-Z0-9-_]{3,}$"
|
||||
|
||||
users:
|
||||
stars-per-page: 5
|
||||
max-tagline-len: 100
|
||||
author-page-size: 25
|
||||
project-page-size: 5
|
||||
staff-roles:
|
||||
- Hangar_Admin
|
||||
- Hangar_Mod
|
||||
|
Loading…
x
Reference in New Issue
Block a user