diff --git a/frontend/locales/en.ts b/frontend/locales/en.ts
index ca4b5c038..b0eaabd68 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 eba848cde..899cb595d 100644
--- a/frontend/pages/_user.vue
+++ b/frontend/pages/_user.vue
@@ -28,12 +28,34 @@
- {{ user.tagline }}
-
-
-
- mdi-pencil
-
+ {{ user.tagline }}
+ {{ $t('author.addTagline') }}
+
+
+
+ mdi-pencil
+
+
+
+
+ {{
+ $t('general.reset')
+ }}
+
+
@@ -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 92d951356..a64b38bbc 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 69f94a324..f6d45ec51 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 60d5e8593..000000000
--- 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 d49beb8f3..000000000
--- 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 7283a5e00..9e3fa51d0 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 fbbcf7002..b148e657d 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 58644476b..40fd2e2a4 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 7687b56fc..49b8e398d 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 8f8675b7e..aff8b1d00 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) {