From 631ffeb43e21bf2359c97d648a3673ce2112bf16 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Wed, 17 Mar 2021 16:55:48 -0700 Subject: [PATCH] user tagline editing --- frontend/components/mixins.ts | 15 +++- frontend/components/modals/HangarModal.vue | 71 +++++++++++++++++ frontend/components/modals/TextareaModal.vue | 17 +---- frontend/locales/en.ts | 19 +++-- frontend/pages/_user.vue | 76 +++++++++++++++++-- frontend/plugins/utils.ts | 8 +- .../hangar/controller/extras/ApiScope.java | 2 + .../controller/extras/HangarApiRequest.java | 34 --------- .../controller/extras/HangarRequest.java | 39 ---------- .../internal/HangarUserController.java | 52 ++++++++++++- .../controller/internal/ReviewController.java | 3 +- .../io/papermc/hangar/model/db/UserTable.java | 2 + .../permission/PermissionRequired.java | 2 +- .../service/internal/users/UserService.java | 5 ++ 14 files changed, 232 insertions(+), 113 deletions(-) create mode 100644 frontend/components/modals/HangarModal.vue delete mode 100644 src/main/java/io/papermc/hangar/controller/extras/HangarApiRequest.java delete mode 100644 src/main/java/io/papermc/hangar/controller/extras/HangarRequest.java diff --git a/frontend/components/mixins.ts b/frontend/components/mixins.ts index ce7df6ae..565f6116 100644 --- a/frontend/components/mixins.ts +++ b/frontend/components/mixins.ts @@ -85,10 +85,17 @@ export class HangarModal extends Vue { activatorClass!: string; @Watch('dialog') - onToggleView() { - if (typeof this.$refs.modalForm !== 'undefined') { - // @ts-ignore - this.$refs.modalForm.reset(); + onToggleView(val: boolean) { + if (!val) { + this.$nextTick(() => { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + if (typeof this.$refs.modalForm !== 'undefined') { + // @ts-ignore + this.$refs.modalForm.reset(); + } + }); } } } diff --git a/frontend/components/modals/HangarModal.vue b/frontend/components/modals/HangarModal.vue new file mode 100644 index 00000000..e731c83d --- /dev/null +++ b/frontend/components/modals/HangarModal.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/frontend/components/modals/TextareaModal.vue b/frontend/components/modals/TextareaModal.vue index 731190f8..0514aa21 100644 --- a/frontend/components/modals/TextareaModal.vue +++ b/frontend/components/modals/TextareaModal.vue @@ -6,7 +6,7 @@ {{ title }} - + diff --git a/frontend/locales/en.ts b/frontend/locales/en.ts index ca4b5c03..b0eaabd6 100644 --- a/frontend/locales/en.ts +++ b/frontend/locales/en.ts @@ -6,6 +6,7 @@ const msgs: LocaleMessageObject = { submit: 'Submit', save: 'Save', comment: 'Comment', + change: 'Change', donate: 'Donate', continue: 'Continue', create: 'Create', @@ -25,8 +26,8 @@ const msgs: LocaleMessageObject = { projectSearch: { query: 'Search in {0} projects, proudly made by the community...', relevanceSort: 'Sort with relevance', - noProjects: 'There are no projects. :/', - noProjectsFound: 'Found 0 projects. :/', + noProjects: 'There are no projects. 😢', + noProjectsFound: 'Found 0 projects. 😢', }, subtitle: 'A Minecraft package repository', sponsoredBy: 'Sponsored by', @@ -67,9 +68,9 @@ const msgs: LocaleMessageObject = { }, project: { stargazers: 'Stargazers', - noStargazers: 'There are no stargazers on this project yet :/', + noStargazers: 'There are no stargazers on this project yet 😢', watchers: 'Watchers', - noWatchers: 'There are no watchers on this project yet :/', + noWatchers: 'There are no watchers on this project yet 😢', members: 'Members', category: { info: 'Category: {0}', @@ -394,6 +395,8 @@ const msgs: LocaleMessageObject = { stars: 'Stars', orgs: 'Organizations', viewOnForums: 'View on forums ', + taglineLabel: 'User Tagline', + editTagline: 'Edit Tagline', memberSince: 'A member since {0}', numProjects: 'No projects | {0} project | {0} projects', addTagline: 'Add a tagline', @@ -408,6 +411,9 @@ const msgs: LocaleMessageObject = { activity: 'User Activity', admin: 'User Admin', }, + error: { + invalidTagline: 'Invalid tagline', + }, }, linkout: { title: 'External Link Warning', @@ -511,7 +517,10 @@ const msgs: LocaleMessageObject = { addVersion: 'Add Version', saveChanges: 'Save Changes', }, - message: 'Good morning!', + validation: { + required: '{0} is required', + maxLength: 'Max length is {0}', + }, }; export default msgs; diff --git a/frontend/pages/_user.vue b/frontend/pages/_user.vue index eba848cd..899cb595 100644 --- a/frontend/pages/_user.vue +++ b/frontend/pages/_user.vue @@ -28,12 +28,34 @@
- - - - - mdi-pencil - + {{ user.tagline }} + {{ $t('author.addTagline') }} + + + + +
@@ -41,7 +63,7 @@ {{ $tc('author.numProjects', user.projectCount, [user.projectCount]) }} {{ $t('author.memberSince', [$util.prettyDate(user.joinDate)]) }} - {{ $t('author.viewOnForums') }}mdi-open-in-new + {{ $t('author.viewOnForums') }}mdi-open-in-new @@ -54,6 +76,7 @@ import { Component, Vue } from 'nuxt-property-decorator'; import { HangarUser } from 'hangar-internal'; import { Context } from '@nuxt/types'; import UserAvatar from '../components/UserAvatar.vue'; +import HangarModal from '~/components/modals/HangarModal.vue'; interface Button { icon: string; @@ -64,10 +87,14 @@ interface Button { } @Component({ - components: { UserAvatar }, + components: { HangarModal, UserAvatar }, }) export default class UserParentPage extends Vue { user!: HangarUser; + taglineForm: string | null = null; + loading = { + resetTagline: false, + }; get buttons(): Button[] { const buttons = [] as Button[]; @@ -80,11 +107,44 @@ export default class UserParentPage extends Vue { return buttons; } + get canEditCurrent() { + return this.user.id === this.$store.state.auth.user.id /* || org perms */; + } + get avatarClazz(): String { return 'user-avatar-md'; // todo check org an add 'organization-avatar' } + changeTagline() { + return this.$api + .requestInternal(`users/${this.user.id}/settings/tagline`, true, 'post', { + content: this.taglineForm, + }) + .then(() => { + this.$nuxt.refresh(); + }) + .catch(this.$util.handleRequestError); + } + + $refs!: { + taglineModal: HangarModal; + }; + + resetTagline() { + this.loading.resetTagline = true; + this.$api + .requestInternal(`users/${this.user.id}/settings/resetTagline`, true, 'post') + .then(() => { + this.$refs.taglineModal.close(); + this.$nuxt.refresh(); + }) + .catch(this.$util.handleRequestError) + .finally(() => { + this.loading.resetTagline = false; + }); + } + async asyncData({ $api, $util, params }: Context) { const user = await $api.requestInternal(`users/${params.user}`, false).catch($util.handlePageRequestError); if (typeof user === 'undefined') return; diff --git a/frontend/plugins/utils.ts b/frontend/plugins/utils.ts index 92d95135..a64b38bb 100644 --- a/frontend/plugins/utils.ts +++ b/frontend/plugins/utils.ts @@ -14,7 +14,6 @@ import { NotifPayload } from '~/store/snackbar'; import { AuthState } from '~/store/auth'; type Validation = (v: any) => boolean | string; -type ValidationArgument = (any: any) => Validation; function handleRequestError(err: AxiosError, error: Context['error'], i18n: Context['app']['i18n']) { if (!err.isAxiosError) { @@ -220,9 +219,10 @@ const createUtil = ({ store, error, app: { i18n } }: Context) => { } } - $vc: Record = { - require: ((name: string | TranslateResult = 'Field') => (v: string) => !!v || `${name} is required`) as ValidationArgument, - requireNonEmptyArray: ((name: string | TranslateResult = 'Field') => (v: any[]) => v.length > 0 || `${name} is required`) as ValidationArgument, + $vc = { + require: (name: TranslateResult = 'Field') => (v: string) => !!v || i18n.t('validation.required', [name]), + maxLength: (maxLength: number) => (v: string) => (!!v && v.length <= maxLength) || i18n.t('validation.maxLength', [maxLength]), + requireNonEmptyArray: (name: TranslateResult = 'Field') => (v: any[]) => v.length > 0 || i18n.t('validation.required', [name]), }; $v: Record = {}; diff --git a/src/main/java/io/papermc/hangar/controller/extras/ApiScope.java b/src/main/java/io/papermc/hangar/controller/extras/ApiScope.java index 69f94a32..f6d45ec5 100644 --- a/src/main/java/io/papermc/hangar/controller/extras/ApiScope.java +++ b/src/main/java/io/papermc/hangar/controller/extras/ApiScope.java @@ -3,6 +3,7 @@ package io.papermc.hangar.controller.extras; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@Deprecated(forRemoval = true) public class ApiScope { private final ApiScopeType type; @@ -65,6 +66,7 @@ public class ApiScope { return new ApiScope(ApiScopeType.ORGANIZATION, organizationName); } + @Deprecated(forRemoval = true) public enum ApiScopeType { GLOBAL, PROJECT, diff --git a/src/main/java/io/papermc/hangar/controller/extras/HangarApiRequest.java b/src/main/java/io/papermc/hangar/controller/extras/HangarApiRequest.java deleted file mode 100644 index 60d5e859..00000000 --- a/src/main/java/io/papermc/hangar/controller/extras/HangarApiRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.papermc.hangar.controller.extras; - -import io.papermc.hangar.model.common.Permission; -import io.papermc.hangar.model.db.UserTable; -import io.papermc.hangar.model.db.auth.ApiKeyTable; -import org.jdbi.v3.core.mapper.Nested; - -import java.time.OffsetDateTime; - -public class HangarApiRequest extends HangarRequest { - - private final ApiKeyTable apiKeyTable; - private final String session; - private final OffsetDateTime expires; - - public HangarApiRequest(@Nested("u") UserTable userTable, @Nested("ak") ApiKeyTable apiKeyTable, String session, OffsetDateTime expires, Permission globalPermissions) { - super(userTable, globalPermissions); - this.apiKeyTable = apiKeyTable; - this.session = session; - this.expires = expires; - } - - public ApiKeyTable getApiKeyTable() { - return apiKeyTable; - } - - public String getSession() { - return session; - } - - public OffsetDateTime getExpires() { - return expires; - } -} diff --git a/src/main/java/io/papermc/hangar/controller/extras/HangarRequest.java b/src/main/java/io/papermc/hangar/controller/extras/HangarRequest.java deleted file mode 100644 index d49beb8f..00000000 --- a/src/main/java/io/papermc/hangar/controller/extras/HangarRequest.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.papermc.hangar.controller.extras; - -import io.papermc.hangar.model.common.Permission; -import io.papermc.hangar.model.db.UserTable; - -public class HangarRequest { - - private final UserTable userTable; - private final Permission globalPermissions; - - public HangarRequest(UserTable userTable, Permission globalPermissions) { - this.userTable = userTable; - this.globalPermissions = globalPermissions; - } - - public boolean hasUser() { - return userTable != null; - } - - public Long getUserId() { - return userTable == null ? null : userTable.getId(); - } - - public UserTable getUserTable() { - return userTable; - } - - public Permission getGlobalPermissions() { - return globalPermissions; - } - - @Override - public String toString() { - return "HangarRequest{" + - "userTable=" + userTable + - ", globalPermissions=" + globalPermissions + - '}'; - } -} diff --git a/src/main/java/io/papermc/hangar/controller/internal/HangarUserController.java b/src/main/java/io/papermc/hangar/controller/internal/HangarUserController.java index 7283a5e0..9e3fa51d 100644 --- a/src/main/java/io/papermc/hangar/controller/internal/HangarUserController.java +++ b/src/main/java/io/papermc/hangar/controller/internal/HangarUserController.java @@ -3,19 +3,29 @@ package io.papermc.hangar.controller.internal; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.papermc.hangar.controller.HangarController; +import io.papermc.hangar.db.customtypes.LoggedActionType; +import io.papermc.hangar.db.customtypes.LoggedActionType.UserContext; import io.papermc.hangar.exceptions.HangarApiException; +import io.papermc.hangar.model.common.NamedPermission; import io.papermc.hangar.model.common.roles.Role; +import io.papermc.hangar.model.db.UserTable; import io.papermc.hangar.model.db.roles.ExtendedRoleTable; +import io.papermc.hangar.model.internal.api.requests.StringContent; import io.papermc.hangar.model.internal.user.HangarUser; import io.papermc.hangar.model.internal.user.notifications.HangarNotification; import io.papermc.hangar.security.HangarAuthenticationToken; +import io.papermc.hangar.security.annotations.permission.PermissionRequired; +import io.papermc.hangar.security.annotations.unlocked.Unlocked; import io.papermc.hangar.service.api.UsersApiService; import io.papermc.hangar.service.internal.roles.MemberService; +import io.papermc.hangar.service.internal.roles.MemberService.OrganizationMemberService; +import io.papermc.hangar.service.internal.roles.MemberService.ProjectMemberService; import io.papermc.hangar.service.internal.roles.OrganizationRoleService; import io.papermc.hangar.service.internal.roles.ProjectRoleService; import io.papermc.hangar.service.internal.roles.RoleService; import io.papermc.hangar.service.internal.users.InviteService; import io.papermc.hangar.service.internal.users.NotificationService; +import io.papermc.hangar.service.internal.users.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -25,10 +35,12 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; +import javax.validation.Valid; import java.util.List; @Controller @@ -38,6 +50,7 @@ public class HangarUserController extends HangarController { private final ObjectMapper mapper; private final UsersApiService usersApiService; + private final UserService userService; private final NotificationService notificationService; private final InviteService inviteService; private final ProjectRoleService projectRoleService; @@ -46,9 +59,10 @@ public class HangarUserController extends HangarController { private final MemberService.OrganizationMemberService organizationMemberService; @Autowired - public HangarUserController(ObjectMapper mapper, UsersApiService usersApiService, NotificationService notificationService, InviteService inviteService, ProjectRoleService projectRoleService, OrganizationRoleService organizationRoleService, MemberService.ProjectMemberService projectMemberService, MemberService.OrganizationMemberService organizationMemberService) { + public HangarUserController(ObjectMapper mapper, UsersApiService usersApiService, UserService userService, NotificationService notificationService, InviteService inviteService, ProjectRoleService projectRoleService, OrganizationRoleService organizationRoleService, ProjectMemberService projectMemberService, OrganizationMemberService organizationMemberService) { this.mapper = mapper; this.usersApiService = usersApiService; + this.userService = userService; this.notificationService = notificationService; this.inviteService = inviteService; this.projectRoleService = projectRoleService; @@ -67,6 +81,42 @@ public class HangarUserController extends HangarController { return ResponseEntity.ok(usersApiService.getUser(userName, HangarUser.class)); } + @Unlocked + @ResponseStatus(HttpStatus.OK) + @PermissionRequired(perms = NamedPermission.EDIT_OWN_USER_SETTINGS) + @PostMapping(path = "/users/{userId}/settings/tagline", consumes = MediaType.APPLICATION_JSON_VALUE) + public void saveTagline(@PathVariable long userId, @Valid @RequestBody StringContent content) { + UserTable userTable = userService.getUserTable(userId); + if (userTable == null) { + throw new HangarApiException(HttpStatus.NOT_FOUND); + } + if (content.getContent().length() > hangarConfig.user.getMaxTaglineLen()) { + throw new HangarApiException(HttpStatus.BAD_REQUEST, "author.error.invalidTagline"); + } + String oldTagline = userTable.getTagline() == null ? "" : userTable.getTagline(); + userTable.setTagline(content.getContent()); + userService.updateUser(userTable); + userActionLogService.user(LoggedActionType.USER_TAGLINE_CHANGED.with(UserContext.of(userId)), userTable.getTagline(), oldTagline); + } + + @Unlocked + @ResponseStatus(HttpStatus.OK) + @PermissionRequired(perms = NamedPermission.EDIT_OWN_USER_SETTINGS) + @PostMapping("/users/{userId}/settings/resetTagline") + public void resetTagline(@PathVariable long userId) { + UserTable userTable = userService.getUserTable(userId); + if (userTable == null) { + throw new HangarApiException(HttpStatus.NOT_FOUND); + } + String oldTagline = userTable.getTagline(); + if (oldTagline == null) { + throw new HangarApiException(HttpStatus.BAD_REQUEST); + } + userTable.setTagline(null); + userService.updateUser(userTable); + userActionLogService.user(LoggedActionType.USER_TAGLINE_CHANGED.with(UserContext.of(userId)), "", oldTagline); + } + @GetMapping("/notifications") public ResponseEntity> getUserNotifications() { return ResponseEntity.ok(notificationService.getUsersNotifications()); diff --git a/src/main/java/io/papermc/hangar/controller/internal/ReviewController.java b/src/main/java/io/papermc/hangar/controller/internal/ReviewController.java index fbbcf700..b148e657 100644 --- a/src/main/java/io/papermc/hangar/controller/internal/ReviewController.java +++ b/src/main/java/io/papermc/hangar/controller/internal/ReviewController.java @@ -1,7 +1,6 @@ package io.papermc.hangar.controller.internal; import io.papermc.hangar.model.common.NamedPermission; -import io.papermc.hangar.model.common.PermissionType; import io.papermc.hangar.model.common.ReviewAction; import io.papermc.hangar.model.common.projects.ReviewState; import io.papermc.hangar.model.internal.api.requests.versions.ReviewMessage; @@ -28,7 +27,7 @@ import java.util.List; @Controller @Secured("ROLE_USER") @RequestMapping(path = "/api/internal/reviews") -@PermissionRequired(type = PermissionType.GLOBAL, perms = NamedPermission.REVIEWER) +@PermissionRequired(perms = NamedPermission.REVIEWER) public class ReviewController { private final ReviewService reviewService; diff --git a/src/main/java/io/papermc/hangar/model/db/UserTable.java b/src/main/java/io/papermc/hangar/model/db/UserTable.java index 58644476..40fd2e2a 100644 --- a/src/main/java/io/papermc/hangar/model/db/UserTable.java +++ b/src/main/java/io/papermc/hangar/model/db/UserTable.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import io.papermc.hangar.model.db.projects.ProjectOwner; import org.jdbi.v3.core.mapper.PropagateNull; import org.jdbi.v3.core.mapper.reflect.JdbiConstructor; +import org.jetbrains.annotations.Nullable; import java.time.OffsetDateTime; import java.util.List; @@ -69,6 +70,7 @@ public class UserTable extends Table implements ProjectOwner { this.email = email; } + @Nullable public String getTagline() { return tagline; } diff --git a/src/main/java/io/papermc/hangar/security/annotations/permission/PermissionRequired.java b/src/main/java/io/papermc/hangar/security/annotations/permission/PermissionRequired.java index 7687b56f..49b8e398 100644 --- a/src/main/java/io/papermc/hangar/security/annotations/permission/PermissionRequired.java +++ b/src/main/java/io/papermc/hangar/security/annotations/permission/PermissionRequired.java @@ -13,7 +13,7 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Documented public @interface PermissionRequired { - PermissionType type(); + PermissionType type() default PermissionType.GLOBAL; NamedPermission[] perms(); String args() default "{}"; } diff --git a/src/main/java/io/papermc/hangar/service/internal/users/UserService.java b/src/main/java/io/papermc/hangar/service/internal/users/UserService.java index 8f8675b7..aff8b1d0 100644 --- a/src/main/java/io/papermc/hangar/service/internal/users/UserService.java +++ b/src/main/java/io/papermc/hangar/service/internal/users/UserService.java @@ -60,6 +60,11 @@ public class UserService extends HangarService { hangarUsersDAO.setNotStarred(projectId, getHangarPrincipal().getUserId()); } } + + public void updateUser(UserTable userTable) { + userDAO.update(userTable); + } + @Nullable private UserTable getUserTable(@Nullable T identifier, @NotNull Function userTableFunction) { if (identifier == null) {