better notification handling for joinables

* work on #395
This commit is contained in:
Jake Potrebic 2021-04-02 16:06:59 -07:00
parent f6e608cde9
commit 870cd9b215
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
29 changed files with 200 additions and 198 deletions

View File

@ -53,6 +53,12 @@ const msgs: LocaleMessageObject = {
log: 'User Action Log',
platformVersions: 'Platform Versions',
logout: 'Sign out',
error: {
loginFailed: 'Authentication Failed',
hangarAuth: "Couldn't connect to HangarAuth",
loginDisabled: 'Login is temporarily unavailable, please try again later',
fakeUserEnabled: 'Fake user is enabled. {0} is therefore disabled',
},
},
createNew: 'Create new...',
new: {
@ -453,11 +459,17 @@ const msgs: LocaleMessageObject = {
project: {
reviewed: '{0} {1} has been reviewed and is approved',
reviewedPartial: '{0} {1} has been reviewed and is partially approved',
invite: 'You have been invited to join the group {0} on the project {1}',
newVersion: 'A new version has been released for {0}: {1}',
invite: 'You have been invited to join the group {0} on the project {1}',
inviteRescinded: 'Your invite to you the group {0} in the project {1} has been rescinded',
removed: 'You have been removed from the group {0} in the project {1}',
roleChanged: 'You have been added to the {0} group in the project {1}',
},
organization: {
invite: 'You have been invited to join the group {0} in the organization {1}',
inviteRescinded: 'Your invite to you the group {0} in the organization {1} has been rescinded',
removed: 'You have been removed from the group {0} in the organization {1}',
roleChanged: 'You have been added to the {0} group in the organization {1}',
},
},
visibility: {

View File

@ -5,7 +5,6 @@ declare module 'hangar-internal' {
interface HangarNotification {
id: number;
type: 'PROJECT_INVITE' | 'ORGANIZATION_INVITE' | 'NEW_PROJECT_VERSION' | 'VERSION_REVIEWED';
action: string;
message: string[];
read: boolean;

View File

@ -152,7 +152,7 @@ public class HangarUserController extends HangarController {
updateRole(organizationRoleService, organizationInviteService, id, status);
}
private <RT extends ExtendedRoleTable<? extends Role<RT>>, RS extends RoleService<RT, ?, ?>, IS extends InviteService<?, RT>> void updateRole(RS roleService, IS inviteService, long id, InviteStatus status) {
private <RT extends ExtendedRoleTable<? extends Role<RT>>, RS extends RoleService<RT, ?, ?>, IS extends InviteService<?, RT, ?>> void updateRole(RS roleService, IS inviteService, long id, InviteStatus status) {
RT table = roleService.getRole(id);
if (table == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);

View File

@ -1,7 +1,7 @@
package io.papermc.hangar.controller.internal;
import io.papermc.hangar.controller.HangarController;
import io.papermc.hangar.exceptions.HangarException;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.api.auth.RefreshResponse;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.internal.sso.AuthUser;
@ -12,7 +12,6 @@ import io.papermc.hangar.service.TokenService;
import io.papermc.hangar.service.internal.auth.SSOService;
import io.papermc.hangar.service.internal.perms.roles.GlobalRoleService;
import io.papermc.hangar.service.internal.users.UserService;
import io.papermc.hangar.util.AlertUtil;
import io.papermc.hangar.util.Routes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -49,7 +48,7 @@ public class LoginController extends HangarController {
}
@GetMapping(path = "/login", params = "returnUrl")
public Object loginFromFrontend(@RequestParam(defaultValue = Routes.Paths.SHOW_HOME) String returnUrl, RedirectAttributes attributes) {
public Object loginFromFrontend(@RequestParam(defaultValue = Routes.Paths.SHOW_HOME) String returnUrl) {
if (config.fakeUser.isEnabled()) {
config.checkDev();
@ -57,13 +56,8 @@ public class LoginController extends HangarController {
tokenService.createTokenForUser(fakeUser);
return new RedirectView(returnUrl);
} else {
try {
response.addCookie(new Cookie("url", returnUrl));
return redirectToSso(ssoService.getLoginUrl(config.getBaseUrl() + "/login"), attributes);
} catch (HangarException e) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, e.getMessageKey(), e.getArgs());
return Routes.SHOW_HOME.getRedirect();
}
response.addCookie(new Cookie("url", returnUrl));
return redirectToSso(ssoService.getLoginUrl(config.getBaseUrl() + "/login"));
}
}
@ -71,8 +65,7 @@ public class LoginController extends HangarController {
public ModelAndView loginFromAuth(@RequestParam String sso, @RequestParam String sig, @CookieValue String url, RedirectAttributes attributes) {
AuthUser authUser = ssoService.authenticate(sso, sig);
if (authUser == null) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, "error.loginFailed");
return Routes.SHOW_HOME.getRedirect();
throw new HangarApiException("nav.user.error.loginFailed");
}
UserTable user = userService.getOrCreate(authUser.getUserName(), authUser);
@ -93,66 +86,28 @@ public class LoginController extends HangarController {
tokenService.invalidateToken(refreshToken);
}
// @GetMapping("/login")
// public ModelAndView login(@RequestParam(defaultValue = "") String sso, @RequestParam(defaultValue = "") String sig, @RequestParam(defaultValue = "") String returnUrl, @CookieValue(value = "url", required = false) String redirectUrl, RedirectAttributes attributes) {
// if (hangarConfig.fakeUser.isEnabled()) {
// hangarConfig.checkDebug();
//
// UserTable fakeUser = authenticationService.loginAsFakeUser();
//
// return redirectBackOnSuccessfulLogin(returnUrl, fakeUser);
// } else if (sso.isEmpty()) {
// String returnPath = returnUrl.isBlank() ? request.getRequestURI() : returnUrl;
// try {
// response.addCookie(new Cookie("url", returnPath));
// return redirectToSso(ssoService.getLoginUrl(hangarConfig.getBaseUrl() + "/login"), attributes);
// } catch (HangarException e) {
// AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, e.getMessageKey(), e.getArgs());
// return Routes.SHOW_HOME.getRedirect();
// }
//
// } else {
// AuthUser authUser = ssoService.authenticate(sso, sig);
// if (authUser == null) {
// AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, "error.loginFailed");
// return Routes.SHOW_HOME.getRedirect();
// }
//
// UserTable user = userService.getOrCreate(authUser.getUserName(), authUser);
// roleService.removeAllGlobalRoles(user.getId());
// authUser.getGlobalRoles().forEach(globalRole -> roleService.addRole(globalRole.create(null, user.getId(), true)));
// authenticationService.setAuthenticatedUser(user);
//
// String redirectPath = redirectUrl != null ? redirectUrl : Routes.getRouteUrlOf("showHome");
// return redirectBackOnSuccessfulLogin(redirectPath, user);
// }
// }
// TODO needed?
@PostMapping("/verify")
public ModelAndView verify(@RequestParam String returnPath, RedirectAttributes attributes) {
try {
return redirectToSso(ssoService.getVerifyUrl(config.getBaseUrl() + returnPath), attributes);
} catch (HangarException e) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, e.getMessageKey(), e.getArgs());
return Routes.SHOW_HOME.getRedirect();
public ModelAndView verify(@RequestParam String returnPath) {
if (config.fakeUser.isEnabled()) {
throw new HangarApiException("nav.user.error.fakeUserEnabled", "Verififcation");
}
return redirectToSso(ssoService.getVerifyUrl(config.getBaseUrl() + returnPath));
}
// TODO needed?
@GetMapping("/logout")
public ModelAndView logout(HttpSession session) {
// TODO flash
session.invalidate();
return Routes.getRedirectToUrl(config.getAuthUrl() + "/accounts/logout/");
}
@GetMapping("/signup")
public ModelAndView signUp(@RequestParam(defaultValue = "") String returnUrl, RedirectAttributes attributes) {
try {
return redirectToSso(ssoService.getSignupUrl(returnUrl), attributes);
} catch (HangarException e) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, e.getMessageKey(), e.getArgs());
return Routes.SHOW_HOME.getRedirect();
public ModelAndView signUp(@RequestParam(defaultValue = "") String returnUrl) {
if (config.fakeUser.isEnabled()) {
throw new HangarApiException("nav.user.error.fakeUserEnabled", "Signup");
}
return redirectToSso(ssoService.getSignupUrl(returnUrl));
}
private ModelAndView redirectBackOnSuccessfulLogin(String url, UserTable user) {
@ -163,14 +118,12 @@ public class LoginController extends HangarController {
url = config.getBaseUrl() + "/" + url;
}
}
// response.addCookie(CookieUtils.builder(HangarAuthenticationFilter.AUTH_NAME, tokenService.expiring()));
return Routes.getRedirectToUrl(url);
}
private ModelAndView redirectToSso(URLWithNonce urlWithNonce, RedirectAttributes attributes) {
private ModelAndView redirectToSso(URLWithNonce urlWithNonce) {
if (!config.sso.isEnabled()) {
AlertUtil.showAlert(attributes, AlertUtil.AlertType.ERROR, "error.noLogin");
return Routes.SHOW_HOME.getRedirect();
throw new HangarApiException("nav.user.error.loginDisabled");
}
ssoService.insert(urlWithNonce.getNonce());
return Routes.getRedirectToUrl(urlWithNonce.getUrl());

View File

@ -8,7 +8,7 @@ import io.papermc.hangar.db.customtypes.RoleCategory;
import io.papermc.hangar.db.modelold.OrganizationsTable;
import io.papermc.hangar.db.modelold.UsersTable;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.internal.user.notifications.NotificationType;
import io.papermc.hangar.modelold.NotificationType;
import io.papermc.hangar.modelold.viewhelpers.UserData;
import io.papermc.hangar.securityold.annotations.OrganizationPermission;
import io.papermc.hangar.serviceold.AuthenticationService;

View File

@ -42,7 +42,7 @@ public class PGLoggedAction extends PGobject {
public static final PGLoggedAction VERSION_PLUGIN_DEPENDENCY_EDITED = new PGLoggedAction("version_plugin_dependency_edited");
public static final PGLoggedAction VERSION_PLUGIN_DEPENDENCY_REMOVED = new PGLoggedAction("version_plugin_dependency_removed");
public static final PGLoggedAction VERSION_PLATFORM_DEPENDENCY_ADDED = new PGLoggedAction("version_platform_dependency_added");
// public static final PGLoggedAction VERSION_PLATFORM_DEPENDENCY_CHANGED = new PGLoggedAction("version_platform_dependency_changed"); // TODO add to sql
// public static final PGLoggedAction VERSION_PLATFORM_DEPENDENCY_CHANGED = new PGLoggedAction("version_platform_dependency_changed"); // TODO add to sql (is this needed? you only ever add or remove them, not change them)
public static final PGLoggedAction VERSION_PLATFORM_DEPENDENCY_REMOVED = new PGLoggedAction("version_platform_dependency_removed");
// Users

View File

@ -12,7 +12,7 @@ import java.util.List;
public interface HangarNotificationsDAO {
@RegisterConstructorMapper(HangarNotification.class)
@SqlQuery("SELECT n.id, n.notification_type AS type, n.action, n.message_args message, n.read, u.name as origin_user_name" +
@SqlQuery("SELECT n.id, n.action, n.message_args message, n.read, u.name as origin_user_name" +
" FROM notifications n" +
" LEFT OUTER JOIN users u ON u.id = n.origin_id" +
" WHERE n.user_id = :userId" +

View File

@ -18,11 +18,11 @@ public interface NotificationsDAO {
@Timestamped
@GetGeneratedKeys
@SqlUpdate("INSERT INTO notifications (created_at, user_id, notification_type, action, origin_id, message_args) VALUES (:now, :userId, :notificationType, :action, :originId, :messageArgs)")
@SqlUpdate("INSERT INTO notifications (created_at, user_id, action, origin_id, message_args) VALUES (:now, :userId, :action, :originId, :messageArgs)")
NotificationTable insert(@BindBean NotificationTable notificationTable);
@Timestamped
@SqlBatch("INSERT INTO notifications (created_at, user_id, notification_type, action, origin_id, message_args) VALUES (:now, :userId, :notificationType, :action, :originId, :messageArgs)")
@SqlBatch("INSERT INTO notifications (created_at, user_id, action, origin_id, message_args) VALUES (:now, :userId, :action, :originId, :messageArgs)")
void insert(@BindBean Collection<NotificationTable> notificationTables);
@SqlUpdate("UPDATE notifications SET read = TRUE WHERE id = :notificationId AND user_id = :userId")

View File

@ -1,7 +1,7 @@
package io.papermc.hangar.db.modelold;
import io.papermc.hangar.model.internal.user.notifications.NotificationType;
import io.papermc.hangar.modelold.NotificationType;
import org.jdbi.v3.core.enums.EnumByOrdinal;
import java.time.OffsetDateTime;

View File

@ -1,7 +1,5 @@
package io.papermc.hangar.model.db;
import io.papermc.hangar.model.internal.user.notifications.NotificationType;
import org.jdbi.v3.core.enums.EnumByOrdinal;
import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
import java.time.OffsetDateTime;
@ -9,27 +7,23 @@ import java.time.OffsetDateTime;
public class NotificationTable extends Table {
private final long userId;
@EnumByOrdinal
private final NotificationType notificationType;
private final String action;
private final boolean read;
private final Long originId;
private final String[] messageArgs;
@JdbiConstructor
public NotificationTable(OffsetDateTime createdAt, long id, long userId, @EnumByOrdinal NotificationType notificationType, String action, boolean read, Long originId, String[] messageArgs) {
public NotificationTable(OffsetDateTime createdAt, long id, long userId, String action, boolean read, Long originId, String[] messageArgs) {
super(createdAt, id);
this.userId = userId;
this.notificationType = notificationType;
this.action = action;
this.read = read;
this.originId = originId;
this.messageArgs = messageArgs;
}
public NotificationTable(long userId, NotificationType notificationType, String action, Long originId, String[] messageArgs) {
public NotificationTable(long userId, String action, Long originId, String[] messageArgs) {
this.userId = userId;
this.notificationType = notificationType;
this.action = action;
this.read = false;
this.originId = originId;
@ -40,11 +34,6 @@ public class NotificationTable extends Table {
return userId;
}
@EnumByOrdinal
public NotificationType getNotificationType() {
return notificationType;
}
public String getAction() {
return action;
}

View File

@ -1,21 +1,17 @@
package io.papermc.hangar.model.internal.user.notifications;
import org.jdbi.v3.core.enums.EnumByOrdinal;
import java.util.List;
public class HangarNotification {
private final long id;
private final NotificationType type;
private final String action;
private final List<String> message;
private final boolean read;
private final String originUserName;
public HangarNotification(long id, @EnumByOrdinal NotificationType type, String action, List<String> message, boolean read, String originUserName) {
public HangarNotification(long id, String action, List<String> message, boolean read, String originUserName) {
this.id = id;
this.type = type;
this.action = action;
this.message = message;
this.read = read;
@ -26,10 +22,6 @@ public class HangarNotification {
return id;
}
public NotificationType getType() {
return type;
}
public String getAction() {
return action;
}
@ -50,7 +42,6 @@ public class HangarNotification {
public String toString() {
return "Notification{" +
"id=" + id +
", type=" + type +
", action='" + action + '\'' +
", message=" + message +
", read=" + read +

View File

@ -1,29 +0,0 @@
package io.papermc.hangar.model.internal.user.notifications;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
@JsonFormat(shape = Shape.OBJECT)
public enum InviteFilter {
ALL("all", "notification.invite.all"),
PROJECTS("projects", "notification.invite.projects"),
ORGANIZATIONS("organizations", "notification.invite.organizations");
private final String name;
private final String title;
public static final InviteFilter[] VALUES = values();
InviteFilter(String name, String title) {
this.name = name;
this.title = title;
}
public String getName() {
return name;
}
public String getTitle() {
return title;
}
}

View File

@ -0,0 +1,6 @@
package io.papermc.hangar.model.internal.user.notifications;
public interface Notification {
String messageKey();
}

View File

@ -1,5 +1,6 @@
package io.papermc.hangar.model.internal.user.notifications;
package io.papermc.hangar.modelold;
@Deprecated(forRemoval = true)
public enum NotificationType {
PROJECT_INVITE,
@ -7,4 +8,6 @@ public enum NotificationType {
NEW_PROJECT_VERSION,
VERSION_REVIEWED,
VERSION_REVIEWED_PARTIAL,
}

View File

@ -5,7 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.table.auth.UserSignOnDAO;
import io.papermc.hangar.exceptions.HangarException;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.db.auth.UserSignOnTable;
import io.papermc.hangar.model.internal.sso.AuthUser;
import io.papermc.hangar.model.internal.sso.URLWithNonce;
@ -114,7 +114,7 @@ public class SSOService {
return CryptoUtils.hmacSha256(hangarConfig.sso.getSecret(), payload.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
LOGGER.warn("Error while singing sso key", e);
throw new HangarException("error.loginFailed");
throw new HangarApiException("nav.user.error.loginFailed");
}
}
@ -137,9 +137,9 @@ public class SSOService {
return returnUrls.getIfPresent(nonce);
}
public static class SignatureException extends HangarException {
public static class SignatureException extends HangarApiException {
SignatureException(String payload, String signature) {
super("error.spongeauth.auth", payload, signature);
super("nav.user.error.hangarAuth", payload, signature);
}
}
}

View File

@ -115,7 +115,7 @@ public class OrganizationFactory extends HangarService {
organizationMemberService.addNewAcceptedByDefaultMember(OrganizationRole.ORGANIZATION_OWNER.create(organizationTable.getId(), getHangarPrincipal().getId(), true));
List<HangarApiException> errors = new ArrayList<>();
organizationInviteService.sendInvites(errors, members, organizationTable.getId(), organizationTable.getName());
organizationInviteService.sendInvites(errors, members, organizationTable);
if (!errors.isEmpty()) {
throw new MultiHangarApiException(errors);

View File

@ -18,7 +18,6 @@ import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.internal.perms.members.OrganizationMemberService;
import io.papermc.hangar.service.internal.users.invites.OrganizationInviteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@ -79,9 +78,9 @@ public class OrganizationService extends HangarService {
OrganizationTable organizationTable = getOrganizationTable(name);
List<HangarApiException> errors = new ArrayList<>();
organizationInviteService.sendInvites(errors, editMembersForm.getNewInvitees(), organizationTable.getId(), organizationTable.getName());
organizationMemberService.editMembers(errors, editMembersForm.getEditedMembers(), organizationTable.getId());
organizationMemberService.removeMembers(errors, editMembersForm.getDeletedMembers(), organizationTable.getId());
organizationInviteService.sendInvites(errors, editMembersForm.getNewInvitees(), organizationTable);
organizationMemberService.editMembers(errors, editMembersForm.getEditedMembers(), organizationTable);
organizationMemberService.removeMembers(errors, editMembersForm.getDeletedMembers(), organizationTable);
if (!errors.isEmpty()) {
throw new MultiHangarApiException(errors);

View File

@ -5,15 +5,19 @@ import io.papermc.hangar.db.dao.internal.table.UserDAO;
import io.papermc.hangar.db.dao.internal.table.members.MembersDAO;
import io.papermc.hangar.db.dao.internal.table.roles.IRolesDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.Named;
import io.papermc.hangar.model.common.roles.Role;
import io.papermc.hangar.model.db.Table;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.members.MemberTable;
import io.papermc.hangar.model.db.roles.ExtendedRoleTable;
import io.papermc.hangar.model.internal.api.requests.EditMembersForm.Member;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.internal.perms.roles.RoleService;
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@ -23,6 +27,8 @@ public abstract class MemberService<
RT extends ExtendedRoleTable<R>,
RD extends IRolesDAO<RT>,
S extends RoleService<RT, R, RD>,
J extends Table & Named,
JNS extends JoinableNotificationService<RT, J>,
MD extends MembersDAO<MT>,
MT extends MemberTable
> extends HangarService {
@ -30,14 +36,16 @@ public abstract class MemberService<
@Autowired
private HangarDao<UserDAO> userDAO;
protected final S roleService;
protected final MD membersDao;
protected final MemberTableConstructor<MT> constructor;
private final S roleService;
private final MD membersDao;
private final JNS joinableNotificationService;
private final MemberTableConstructor<MT> constructor;
private final String errorPrefix;
protected MemberService(S roleService, MD membersDao, MemberTableConstructor<MT> constructor, String errorPrefix) {
protected MemberService(S roleService, MD membersDao, JNS joinableNotificationService, MemberTableConstructor<MT> constructor, String errorPrefix) {
this.roleService = roleService;
this.membersDao = membersDao;
this.joinableNotificationService = joinableNotificationService;
this.constructor = constructor;
this.errorPrefix = errorPrefix;
}
@ -74,10 +82,11 @@ public abstract class MemberService<
logMemberRemoval(roleTable.getPrincipalId(), sb);
}
public void removeMembers(List<HangarApiException> errors, List<Member<R>> members, long principalId) {
@Transactional
public void removeMembers(List<HangarApiException> errors, List<Member<R>> members, J joinable) {
List<RT> toBeRemoved = new ArrayList<>();
StringBuilder sb = new StringBuilder("Removed: ");
handleEditOrRemoval(errors, toBeRemoved, members, principalId, (member, rt, notLast) -> {
handleEditOrRemoval(errors, toBeRemoved, members, joinable.getId(), (member, rt, notLast) -> {
sb.append(member.getName()).append(" (").append(member.getRole().getTitle()).append(")");
if (notLast) {
sb.append(", ");
@ -88,19 +97,20 @@ public abstract class MemberService<
membersDao.delete(rt.getPrincipalId(), rt.getUserId());
roleService.deleteRole(rt);
}
// TODO notifications for removal
if (!toBeRemoved.isEmpty()) {
logMemberRemoval(principalId, sb.toString());
joinableNotificationService.removedFrom(toBeRemoved, joinable);
logMemberRemoval(joinable.getId(), sb.toString());
}
}
abstract void logMemberRemoval(long principalId, String logEntry);
public void editMembers(List<HangarApiException> errors, List<Member<R>> members, long principalId) {
@Transactional
public void editMembers(List<HangarApiException> errors, List<Member<R>> members, J joinable) {
List<RT> toBeUpdated = new ArrayList<>();
StringBuilder oldState = new StringBuilder("Old Roles: ");
StringBuilder newState = new StringBuilder("New Roles: ");
handleEditOrRemoval(errors, toBeUpdated, members, principalId, (member, rt, notLast) -> {
handleEditOrRemoval(errors, toBeUpdated, members, joinable.getId(), (member, rt, notLast) -> {
if (member.getRole() == rt.getRole()) {
return false;
}
@ -118,9 +128,9 @@ public abstract class MemberService<
for (RT rt : toBeUpdated) {
roleService.updateRoles(toBeUpdated);
}
// TODO notifications for new role
if (!toBeUpdated.isEmpty()) {
logMemberUpdate(principalId, oldState.toString(), newState.toString());
joinableNotificationService.roleChanged(toBeUpdated, joinable);
logMemberUpdate(joinable.getId(), oldState.toString(), newState.toString());
}
}

View File

@ -4,12 +4,14 @@ import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.table.members.OrganizationMembersDAO;
import io.papermc.hangar.db.dao.internal.table.roles.OrganizationRolesDAO;
import io.papermc.hangar.model.common.roles.OrganizationRole;
import io.papermc.hangar.model.db.OrganizationTable;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.members.OrganizationMemberTable;
import io.papermc.hangar.model.db.roles.OrganizationRoleTable;
import io.papermc.hangar.model.internal.logs.LogAction;
import io.papermc.hangar.model.internal.logs.contexts.OrganizationContext;
import io.papermc.hangar.service.internal.perms.roles.OrganizationRoleService;
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService.OrganizationNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -21,13 +23,15 @@ public class OrganizationMemberService extends MemberService<
OrganizationRoleTable,
OrganizationRolesDAO,
OrganizationRoleService,
OrganizationTable,
OrganizationNotificationService,
OrganizationMembersDAO,
OrganizationMemberTable
> {
@Autowired
public OrganizationMemberService(OrganizationRoleService roleService, HangarDao<OrganizationMembersDAO> organizationMembersDAO) {
super(roleService, organizationMembersDAO.get(), OrganizationMemberTable::new, "organization.settings.members.");
public OrganizationMemberService(OrganizationRoleService roleService, HangarDao<OrganizationMembersDAO> organizationMembersDAO, OrganizationNotificationService organizationNotificationService) {
super(roleService, organizationMembersDAO.get(), organizationNotificationService, OrganizationMemberTable::new, "organization.settings.members.");
}
@Override

View File

@ -6,10 +6,12 @@ import io.papermc.hangar.db.dao.internal.table.roles.ProjectRolesDAO;
import io.papermc.hangar.model.common.roles.ProjectRole;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.members.ProjectMemberTable;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.db.roles.ProjectRoleTable;
import io.papermc.hangar.model.internal.logs.LogAction;
import io.papermc.hangar.model.internal.logs.contexts.ProjectContext;
import io.papermc.hangar.service.internal.perms.roles.ProjectRoleService;
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService.ProjectNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -21,13 +23,15 @@ public class ProjectMemberService extends MemberService<
ProjectRoleTable,
ProjectRolesDAO,
ProjectRoleService,
ProjectTable,
ProjectNotificationService,
ProjectMembersDAO,
ProjectMemberTable
> {
@Autowired
public ProjectMemberService(ProjectRoleService projectRoleService, HangarDao<ProjectMembersDAO> projectMembersDAO) {
super(projectRoleService, projectMembersDAO.get(), ProjectMemberTable::new, "project.settings.error.members.");
public ProjectMemberService(ProjectRoleService projectRoleService, HangarDao<ProjectMembersDAO> projectMembersDAO, ProjectNotificationService projectNotificationService) {
super(projectRoleService, projectMembersDAO.get(), projectNotificationService, ProjectMemberTable::new, "project.settings.error.members.");
}
@Override

View File

@ -197,9 +197,9 @@ public class ProjectService extends HangarService {
ProjectTable projectTable = getProjectTable(author, slug);
List<HangarApiException> errors = new ArrayList<>();
projectInviteService.sendInvites(errors, editMembersForm.getNewInvitees(), projectTable.getId(), projectTable.getName());
projectMemberService.editMembers(errors, editMembersForm.getEditedMembers(), projectTable.getId());
projectMemberService.removeMembers(errors, editMembersForm.getDeletedMembers(), projectTable.getId());
projectInviteService.sendInvites(errors, editMembersForm.getNewInvitees(), projectTable);
projectMemberService.editMembers(errors, editMembersForm.getEditedMembers(), projectTable);
projectMemberService.removeMembers(errors, editMembersForm.getDeletedMembers(), projectTable);
if (!errors.isEmpty()) {
throw new MultiHangarApiException(errors);

View File

@ -5,15 +5,11 @@ import io.papermc.hangar.db.dao.internal.HangarNotificationsDAO;
import io.papermc.hangar.db.dao.internal.table.NotificationsDAO;
import io.papermc.hangar.db.dao.internal.table.projects.ProjectsDAO;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.common.roles.OrganizationRole;
import io.papermc.hangar.model.common.roles.ProjectRole;
import io.papermc.hangar.model.db.NotificationTable;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.internal.api.requests.EditMembersForm.Member;
import io.papermc.hangar.model.internal.user.notifications.HangarNotification;
import io.papermc.hangar.model.internal.user.notifications.NotificationType;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.PermissionService;
import org.springframework.stereotype.Service;
@ -49,7 +45,6 @@ public class NotificationService extends HangarService {
for (UserTable projectWatcher : projectWatchers) {
notificationTables.add(new NotificationTable(
projectWatcher.getId(),
NotificationType.NEW_PROJECT_VERSION,
projectTable.getOwnerName() + "/" + projectTable.getSlug(),
projectTable.getId(),
new String[]{"notifications.project.newVersion", projectTable.getName(), projectVersionTable.getVersionString()})
@ -64,22 +59,14 @@ public class NotificationService extends HangarService {
permissionService.getProjectMemberPermissions(projectVersionTable.getProjectId()).forEach((user, perm) -> {
if (perm.has(Permission.EditVersion)) {
if (partial) {
notificationTables.add(new NotificationTable(user.getId(), NotificationType.VERSION_REVIEWED_PARTIAL, null, null,
notificationTables.add(new NotificationTable(user.getId(), null, null,
new String[]{"notifications.project.reviewedPartial", projectTable.getSlug(), projectVersionTable.getVersionString()}));
} else {
notificationTables.add(new NotificationTable(user.getId(), NotificationType.VERSION_REVIEWED, null, null,
notificationTables.add(new NotificationTable(user.getId(), null, null,
new String[]{"notifications.project.reviewed", projectTable.getSlug(), projectVersionTable.getVersionString()}));
}
}
});
notificationsDAO.insert(notificationTables);
}
public void notifyNewProjectInvite(Member<ProjectRole> member, long userId, long projectId, String projectName) {
notificationsDAO.insert(new NotificationTable(userId, NotificationType.PROJECT_INVITE, null, projectId, new String[]{"notifications.project.invite", member.getRole().getTitle(), projectName}));
}
public void notifyNewOrganizationInvite(Member<OrganizationRole> member, long userId, long organizationId, String organizationName) {
notificationsDAO.insert(new NotificationTable(userId, NotificationType.ORGANIZATION_INVITE, null, organizationId, new String[]{"notifications.organization.invite", member.getRole().getTitle(), organizationName}));
}
}

View File

@ -4,7 +4,9 @@ import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.HangarNotificationsDAO;
import io.papermc.hangar.db.dao.internal.table.UserDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.Named;
import io.papermc.hangar.model.common.roles.Role;
import io.papermc.hangar.model.db.Table;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.roles.ExtendedRoleTable;
import io.papermc.hangar.model.internal.api.requests.EditMembersForm.Member;
@ -12,12 +14,14 @@ import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.internal.perms.members.MemberService;
import io.papermc.hangar.service.internal.perms.roles.RoleService;
import io.papermc.hangar.service.internal.users.NotificationService;
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
public abstract class InviteService<R extends Role<RT>, RT extends ExtendedRoleTable<R>> extends HangarService {
public abstract class InviteService<R extends Role<RT>, RT extends ExtendedRoleTable<R>, J extends Table & Named> extends HangarService {
@Autowired
protected HangarDao<HangarNotificationsDAO> hangarNotificationsDAO;
@ -29,18 +33,21 @@ public abstract class InviteService<R extends Role<RT>, RT extends ExtendedRoleT
private HangarDao<UserDAO> userDAO;
private final RoleService<RT, R, ?> roleService;
private final MemberService<R, RT, ?, ?, ?, ?> memberService;
private final MemberService<R, RT, ?, ?, ?, ?, ?, ?> memberService;
private final JoinableNotificationService<RT, J> joinableNotificationService;
private final String errorPrefix;
protected InviteService(RoleService<RT, R, ?> roleService, MemberService<R, RT, ?, ?, ?, ?> memberService, String errorPrefix) {
protected InviteService(RoleService<RT, R, ?> roleService, MemberService<R, RT, ?, ?, ?, ?, ?, ?> memberService, JoinableNotificationService<RT, J> joinableNotificationService, String errorPrefix) {
this.roleService = roleService;
this.memberService = memberService;
this.joinableNotificationService = joinableNotificationService;
this.errorPrefix = errorPrefix;
}
@Transactional
public void sendInvites(List<HangarApiException> errors, List<Member<R>> invitees, long principalId, String principalName) {
public void sendInvites(List<HangarApiException> errors, List<Member<R>> invitees, J joinable) {
StringBuilder sb = new StringBuilder("Invited: ");
List<RT> toBeInvited = new ArrayList<>();
for (int i = 0; i < invitees.size(); i++) {
Member<R> invitee = invitees.get(i);
UserTable userTable = userDAO.get().getUserTable(invitee.getName());
@ -48,23 +55,23 @@ public abstract class InviteService<R extends Role<RT>, RT extends ExtendedRoleT
errors.add(new HangarApiException(this.errorPrefix + "invalidUser", invitee.getName()));
continue;
}
if (roleService.addRole(invitee.getRole().create(principalId, userTable.getId(), false), true) == null) {
RT rt = roleService.addRole(invitee.getRole().create(joinable.getId(), userTable.getId(), false), true);
if (rt == null) {
errors.add(new HangarApiException(this.errorPrefix + "alreadyInvited", invitee.getName()));
continue;
}
notifyNewInvites(invitee, userTable.getId(), principalId, principalName);
toBeInvited.add(rt);
sb.append(userTable.getName()).append(" (").append(invitee.getRole().getTitle()).append(")");
if (i + 1 != invitees.size()) {
sb.append(", ");
}
}
if (!invitees.isEmpty()) {
logInvitesSent(principalId, sb.toString());
joinableNotificationService.invited(toBeInvited, joinable);
logInvitesSent(joinable.getId(), sb.toString());
}
}
abstract void notifyNewInvites(Member<R> invitee, long userId, long principalId, String principalName);
abstract void logInvitesSent(long principalId, String log);
public void acceptInvite(RT roleTable) {

View File

@ -1,14 +1,15 @@
package io.papermc.hangar.service.internal.users.invites;
import io.papermc.hangar.model.common.roles.OrganizationRole;
import io.papermc.hangar.model.db.OrganizationTable;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.roles.OrganizationRoleTable;
import io.papermc.hangar.model.internal.api.requests.EditMembersForm.Member;
import io.papermc.hangar.model.internal.logs.LogAction;
import io.papermc.hangar.model.internal.logs.contexts.OrganizationContext;
import io.papermc.hangar.model.internal.user.notifications.HangarInvite.HangarOrganizationInvite;
import io.papermc.hangar.service.internal.perms.members.OrganizationMemberService;
import io.papermc.hangar.service.internal.perms.roles.OrganizationRoleService;
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService.OrganizationNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -16,22 +17,17 @@ import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
public class OrganizationInviteService extends InviteService<OrganizationRole, OrganizationRoleTable> {
public class OrganizationInviteService extends InviteService<OrganizationRole, OrganizationRoleTable, OrganizationTable> {
@Autowired
public OrganizationInviteService(OrganizationRoleService roleService, OrganizationMemberService memberService) {
super(roleService, memberService, "organization.settings.members.");
public OrganizationInviteService(OrganizationRoleService roleService, OrganizationMemberService memberService, OrganizationNotificationService organizationNotificationService) {
super(roleService, memberService, organizationNotificationService, "organization.settings.members.");
}
public List<HangarOrganizationInvite> getOrganizationInvites() {
return hangarNotificationsDAO.get().getOrganizationInvites(getHangarPrincipal().getId());
}
@Override
void notifyNewInvites(Member<OrganizationRole> invitee, long userId, long principalId, String principalName) {
notificationService.notifyNewOrganizationInvite(invitee, userId, principalId, principalName);
}
@Override
void logInvitesSent(long principalId, String log) {
userActionLogService.organization(LogAction.ORGANIZATION_INVITES_SENT.create(OrganizationContext.of(principalId), log, ""));

View File

@ -2,13 +2,14 @@ package io.papermc.hangar.service.internal.users.invites;
import io.papermc.hangar.model.common.roles.ProjectRole;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.db.roles.ProjectRoleTable;
import io.papermc.hangar.model.internal.api.requests.EditMembersForm.Member;
import io.papermc.hangar.model.internal.logs.LogAction;
import io.papermc.hangar.model.internal.logs.contexts.ProjectContext;
import io.papermc.hangar.model.internal.user.notifications.HangarInvite.HangarProjectInvite;
import io.papermc.hangar.service.internal.perms.members.ProjectMemberService;
import io.papermc.hangar.service.internal.perms.roles.ProjectRoleService;
import io.papermc.hangar.service.internal.users.notifications.JoinableNotificationService.ProjectNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -16,22 +17,17 @@ import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
public class ProjectInviteService extends InviteService<ProjectRole, ProjectRoleTable> {
public class ProjectInviteService extends InviteService<ProjectRole, ProjectRoleTable, ProjectTable> {
@Autowired
public ProjectInviteService(ProjectRoleService roleService, ProjectMemberService memberService) {
super(roleService, memberService, "project.settings.error.members.");
public ProjectInviteService(ProjectRoleService roleService, ProjectMemberService memberService, ProjectNotificationService projectNotificationService) {
super(roleService, memberService, projectNotificationService, "project.settings.error.members.");
}
public List<HangarProjectInvite> getProjectInvites() {
return hangarNotificationsDAO.get().getProjectInvites(getHangarPrincipal().getId());
}
@Override
void notifyNewInvites(Member<ProjectRole> invitee, long userId, long principalId, String principalName) {
notificationService.notifyNewProjectInvite(invitee, userId, principalId, principalName);
}
@Override
void logInvitesSent(long principalId, String log) {
userActionLogService.project(LogAction.PROJECT_INVITES_SENT.create(ProjectContext.of(principalId), log, ""));

View File

@ -0,0 +1,74 @@
package io.papermc.hangar.service.internal.users.notifications;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.table.NotificationsDAO;
import io.papermc.hangar.model.Named;
import io.papermc.hangar.model.common.roles.Role;
import io.papermc.hangar.model.db.NotificationTable;
import io.papermc.hangar.model.db.OrganizationTable;
import io.papermc.hangar.model.db.Table;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.db.roles.ExtendedRoleTable;
import io.papermc.hangar.model.db.roles.OrganizationRoleTable;
import io.papermc.hangar.model.db.roles.ProjectRoleTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
public abstract class JoinableNotificationService<RT extends ExtendedRoleTable<? extends Role<RT>>, J extends Table & Named> {
@Autowired
private HangarDao<NotificationsDAO> notificationsDAO;
protected final String msgPrefix;
protected JoinableNotificationService(String msgPrefix) {
this.msgPrefix = msgPrefix;
}
public void invited(Collection<RT> inviteeRoleTables, J joinable) {
Collection<NotificationTable> notificationTables = new HashSet<>();
for (RT rt : inviteeRoleTables) {
notificationTables.add(new NotificationTable(rt.getUserId(), null, joinable.getId(), new String[]{ this.msgPrefix + "invite", rt.getRole().getTitle(), joinable.getName()}));
}
notificationsDAO.get().insert(notificationTables);
}
public void removedFrom(Collection<RT> removedFromRoleTables, J joinable) {
Collection<NotificationTable> notificationTables = new HashSet<>();
for (RT rt : removedFromRoleTables) {
String msgKey = this.msgPrefix + (rt.isAccepted() ? "removed" : "inviteRescinded");
notificationTables.add(new NotificationTable(rt.getUserId(), null, joinable.getId(), new String[] {msgKey, rt.getRole().getTitle(), joinable.getName()}));
}
notificationsDAO.get().insert(notificationTables);
}
public void roleChanged(Collection<RT> changedRoleTables, J joinable) {
Collection<NotificationTable> notificationTables = new HashSet<>();
for (RT rt : changedRoleTables) {
notificationTables.add(new NotificationTable(rt.getUserId(), null, joinable.getId(), new String[] {this.msgPrefix + "roleChanged", rt.getRole().getTitle(), joinable.getName()}));
}
notificationsDAO.get().insert(notificationTables);
}
@Service
public static class ProjectNotificationService extends JoinableNotificationService<ProjectRoleTable, ProjectTable> {
public ProjectNotificationService() {
super("notifications.project.");
}
}
@Service
public static class OrganizationNotificationService extends JoinableNotificationService<OrganizationRoleTable, OrganizationTable> {
public OrganizationNotificationService() {
super("notifications.organization.");
}
}
}

View File

@ -15,7 +15,7 @@ import org.springframework.http.HttpStatus;
import java.util.Map.Entry;
public abstract class VisibilityService<M extends Table & ModelVisible & ProjectIdentified, VT extends VisibilityChangeTable> extends HangarService {
abstract class VisibilityService<M extends Table & ModelVisible & ProjectIdentified, VT extends VisibilityChangeTable> extends HangarService {
@Autowired
private PermissionService permissionService;

View File

@ -3,7 +3,7 @@ package io.papermc.hangar.serviceold;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.daoold.NotificationsDao;
import io.papermc.hangar.db.modelold.NotificationsTable;
import io.papermc.hangar.model.internal.user.notifications.NotificationType;
import io.papermc.hangar.modelold.NotificationType;
import org.postgresql.shaded.com.ongres.scram.common.util.Preconditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

View File

@ -0,0 +1 @@
ALTER TABLE notifications DROP COLUMN notification_type;