2
0
mirror of https://github.com/HangarMC/Hangar.git synced 2025-02-23 15:12:52 +08:00

feat: denormalize user avatars

This commit is contained in:
MiniDigger | Martin 2024-12-31 11:55:15 +01:00
parent ae3d0abb08
commit b780050b47
28 changed files with 95 additions and 109 deletions

View File

@ -8,6 +8,7 @@ import io.papermc.hangar.components.auth.model.credential.PasswordCredential;
import io.papermc.hangar.components.auth.model.db.UserCredentialTable;
import io.papermc.hangar.components.auth.model.dto.SignupForm;
import io.papermc.hangar.components.auth.model.dto.login.LoginResponse;
import io.papermc.hangar.components.images.service.AvatarService;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.dao.internal.table.UserDAO;
import io.papermc.hangar.exceptions.HangarApiException;
@ -50,8 +51,9 @@ public class AuthService extends HangarComponent implements UserDetailsService {
private final UsersApiService usersApiService;
private final BucketService bucketService;
private final TurnstileService turnstileService;
private final AvatarService avatarService;
public AuthService(final UserDAO userDAO, final UserCredentialDAO userCredentialDAO, final PasswordEncoder passwordEncoder, final ValidationService validationService, final VerificationService verificationService, final CredentialsService credentialsService, final HibpService hibpService, final MailService mailService, final TokenService tokenService, final UsersApiService usersApiService, final BucketService bucketService, final TurnstileService turnstileService) {
public AuthService(final UserDAO userDAO, final UserCredentialDAO userCredentialDAO, final PasswordEncoder passwordEncoder, final ValidationService validationService, final VerificationService verificationService, final CredentialsService credentialsService, final HibpService hibpService, final MailService mailService, final TokenService tokenService, final UsersApiService usersApiService, final BucketService bucketService, final TurnstileService turnstileService, final AvatarService avatarService) {
this.userDAO = userDAO;
this.userCredentialDAO = userCredentialDAO;
this.passwordEncoder = passwordEncoder;
@ -64,6 +66,7 @@ public class AuthService extends HangarComponent implements UserDetailsService {
this.usersApiService = usersApiService;
this.bucketService = bucketService;
this.turnstileService = turnstileService;
this.avatarService = avatarService;
}
@Transactional
@ -88,7 +91,7 @@ public class AuthService extends HangarComponent implements UserDetailsService {
}
}
final UserTable userTable = this.userDAO.create(UUID.randomUUID(), form.username(), form.email(), null, "en", List.of(), false, "light", emailVerified, new JSONB(Map.of()));
final UserTable userTable = this.userDAO.create(UUID.randomUUID(), form.username(), form.email(), null, "en", List.of(), false, "light", emailVerified, avatarService.getDefaultAvatarUrl(), new JSONB(Map.of()));
this.credentialsService.registerCredential(userTable.getUserId(), new PasswordCredential(this.passwordEncoder.encode(form.password())));
if (!emailVerified) {
this.verificationService.sendVerificationCode(userTable.getUserId(), userTable.getEmail(), userTable.getName());

View File

@ -13,6 +13,7 @@ import io.papermc.hangar.components.auth.model.oauth.OAuthCodeResponse;
import io.papermc.hangar.components.auth.model.oauth.OAuthMode;
import io.papermc.hangar.components.auth.model.oauth.OAuthProvider;
import io.papermc.hangar.components.auth.model.oauth.OAuthUserDetails;
import io.papermc.hangar.components.images.service.AvatarService;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.dao.internal.table.UserDAO;
import io.papermc.hangar.exceptions.HangarApiException;
@ -41,10 +42,11 @@ public class OAuthService extends HangarComponent {
private final VerificationService verificationService;
private final UserCredentialDAO userCredentialDAO;
private final CredentialsService credentialsService;
private final AvatarService avatarService;
private final Map<String, OAuthProvider> providers = new HashMap<>();
public OAuthService(final RestClient restClient, final Algorithm algo, final JWT jwt, final AuthService authService, final UserDAO userDAO, final VerificationService verificationService, final UserCredentialDAO userCredentialDAO, final CredentialsService credentialsService) {
public OAuthService(final RestClient restClient, final Algorithm algo, final JWT jwt, final AuthService authService, final UserDAO userDAO, final VerificationService verificationService, final UserCredentialDAO userCredentialDAO, final CredentialsService credentialsService, final AvatarService avatarService) {
this.restClient = restClient;
this.algo = algo;
this.jwt = jwt;
@ -53,6 +55,7 @@ public class OAuthService extends HangarComponent {
this.verificationService = verificationService;
this.userCredentialDAO = userCredentialDAO;
this.credentialsService = credentialsService;
this.avatarService = avatarService;
}
@PostConstruct
@ -211,7 +214,7 @@ public class OAuthService extends HangarComponent {
throw new HangarApiException("This " + provider + " account is already linked to a Hangar account");
}
final UserTable userTable = this.userDAO.create(UUID.randomUUID(), username, email, null, "en", List.of(), false, "light", emailVerified, new JSONB(Map.of()));
final UserTable userTable = this.userDAO.create(UUID.randomUUID(), username, email, null, "en", List.of(), false, "light", emailVerified, this.avatarService.getDefaultAvatarUrl(), new JSONB(Map.of()));
if (!emailVerified) {
this.verificationService.sendVerificationCode(userTable.getUserId(), userTable.getEmail(), userTable.getName());
}

View File

@ -1,26 +1,19 @@
package io.papermc.hangar.components.images.service;
import com.github.benmanes.caffeine.cache.Cache;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.components.images.controller.AvatarController;
import io.papermc.hangar.components.images.dao.AvatarDAO;
import io.papermc.hangar.components.images.model.AvatarTable;
import io.papermc.hangar.config.CacheConfig;
import io.papermc.hangar.model.api.User;
import io.papermc.hangar.model.db.UserTable;
import io.papermc.hangar.model.internal.user.HangarUser;
import io.papermc.hangar.service.internal.file.FileService;
import io.papermc.hangar.service.internal.users.UserService;
import io.papermc.hangar.util.CryptoUtils;
import jakarta.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
@ -46,17 +39,12 @@ public class AvatarService extends HangarComponent {
private String defaultAvatarUrl;
private String defaultAvatarPath;
private final Cache<String, String> usernameCache;
public AvatarService(final FileService fileService, final ImageService imageService, final AvatarDAO avatarDAO, final UserService userService, @Qualifier(CacheConfig.USERNAME) final org.springframework.cache.Cache usernameCache, final RestTemplate restTemplate) {
public AvatarService(final FileService fileService, final ImageService imageService, final AvatarDAO avatarDAO, final UserService userService, final RestTemplate restTemplate) {
this.fileService = fileService;
this.imageService = imageService;
this.avatarDAO = avatarDAO;
this.userService = userService;
this.restTemplate = restTemplate;
//noinspection unchecked
this.usernameCache = (Cache<String, String>) usernameCache.getNativeCache();
}
@PostConstruct
@ -92,27 +80,10 @@ public class AvatarService extends HangarComponent {
* Get methods
*/
@Deprecated(forRemoval = true)
public String getUserAvatarUrl(final UserTable userTable) {
private String getUserAvatarUrl(final UserTable userTable) {
return this.getAvatarUrl(USER, userTable.getUuid().toString(), null);
}
@Deprecated(forRemoval = true)
public String getUserAvatarUrl(final UUID orgUserUuid) {
return this.getAvatarUrl(USER, orgUserUuid.toString(), null);
}
@Deprecated(forRemoval = true)
public String getUserAvatarUrl(final User user) {
final String uuid;
if (user instanceof final HangarUser hangarUser) {
uuid = hangarUser.getUuid().toString();
} else {
uuid = this.usernameCache.get(user.getName(), (key) -> Objects.requireNonNull(this.userService.getUserTable(user.getName())).getUuid().toString());
}
return this.getAvatarUrl(USER, uuid, null);
}
@Deprecated(forRemoval = true)
public String getProjectAvatarUrl(final long projectId, final String ownerName) {
return this.getAvatarUrl(PROJECT, String.valueOf(projectId), () -> {
@ -149,15 +120,17 @@ public class AvatarService extends HangarComponent {
/*
* change methods
*/
public void changeUserAvatar(final UUID uuid, final byte[] avatar) throws IOException {
this.changeAvatar(USER, uuid.toString(), avatar);
public void changeUserAvatar(final UserTable user, final byte[] avatar) throws IOException {
final String newUrl = this.changeAvatar(USER, user.getUuid().toString(), avatar);
user.setAvatarUrl(newUrl);
this.userService.updateUser(user);
}
public void changeProjectAvatar(final long projectId, final byte[] avatar) throws IOException {
this.changeAvatar(PROJECT, String.valueOf(projectId), avatar);
}
private void changeAvatar(final String type, final String subject, byte[] avatar) throws IOException {
private String changeAvatar(final String type, final String subject, byte[] avatar) throws IOException {
final String unoptimizedHash = CryptoUtils.md5ToHex(avatar);
AvatarTable table = this.avatarDAO.getAvatar(type, subject);
if (table != null && table.getUnoptimizedHash().equals(unoptimizedHash)) {
@ -175,6 +148,7 @@ public class AvatarService extends HangarComponent {
this.avatarDAO.updateAvatar(table);
}
this.fileService.write(new ByteArrayInputStream(avatar), this.getPath(type, subject), AvatarController.WEBP.toString());
return fileService.getAvatarUrl(type, subject, String.valueOf(table.getVersion()));
}
/*
@ -220,4 +194,9 @@ public class AvatarService extends HangarComponent {
return this.fileService.bytes(this.getPath(type, subject));
}
}
public void fixAvatarUrls() {
// TODO implement
//this.avatarDAO.getUsersWithBrokenAvatars();
}
}

View File

@ -40,7 +40,6 @@ public class CacheConfig {
public static final String GLOBAL_SITEMAP = "globalSitemap-cache";
public static final String USER_SITEMAP = "userSitemap-cache";
public static final String AVATARS = "avatars-cache";
public static final String USERNAME = "username-cache";
public static final String VERSION_DEPENDENCIES = "version-dependencies-cache";
public static final String LATEST_VERSION = "latest-version-cache";
@ -174,11 +173,6 @@ public class CacheConfig {
return this.createCache(AVATARS, Duration.ofMinutes(30), 2000);
}
@Bean(USERNAME)
Cache usernameCache() {
return this.createCache(USERNAME, Duration.ofHours(2), 500);
}
@Bean(VERSION_DEPENDENCIES)
Cache versionDependenciesCache() {
return this.createCache(VERSION_DEPENDENCIES, Duration.ofMinutes(30), 2000);

View File

@ -204,7 +204,7 @@ public class HangarUserController extends HangarComponent {
if (table == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown org " + userName);
}
this.avatarService.changeUserAvatar(table.getUuid(), avatar.getBytes());
this.avatarService.changeUserAvatar(table, avatar.getBytes());
}
// @el(userName: String)

View File

@ -205,7 +205,11 @@ public class OrganizationController extends HangarComponent {
if (table == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown org " + orgName);
}
this.avatarService.changeUserAvatar(table.getUserUuid(), avatar.getBytes());
final UserTable userTable = this.userService.getUserTable(table.getUserId());
if (userTable == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown org user " + orgName + " (" + table.getUserId() + ")");
}
this.avatarService.changeUserAvatar(userTable, avatar.getBytes());
}
@Anyone

View File

@ -38,6 +38,7 @@ public interface UsersDAO {
u.language,
u.theme,
u.socials,
u.avatar_url,
exists(SELECT 1 FROM organizations o WHERE u.id = o.user_id) AS is_organization
FROM users u
WHERE lower(u.name) = lower(:name)
@ -67,6 +68,7 @@ public interface UsersDAO {
" u.locked," +
" u.language," +
" u.theme," +
" u.avatar_url," +
" exists(SELECT 1 FROM organizations o WHERE u.id = o.user_id) AS is_organization" +
" FROM users u" +
" <if(hasQuery)>WHERE u.name ILIKE '%' || :query || '%'<endif>" +

View File

@ -21,24 +21,24 @@ public interface UserDAO {
@Timestamped
@GetGeneratedKeys
@SqlUpdate("""
INSERT INTO users (uuid, created_at, name, email, tagline, read_prompts, locked, language, theme, email_verified, socials)
VALUES (:uuid, :now, :name, :email, :tagline, :readPrompts, :locked, :language, :theme, :emailVerified, :socials)
INSERT INTO users (uuid, created_at, name, email, tagline, read_prompts, locked, language, theme, email_verified, avatar_url, socials)
VALUES (:uuid, :now, :name, :email, :tagline, :readPrompts, :locked, :language, :theme, :emailVerified, :avatarUrl, :socials)
""")
UserTable insert(@BindBean UserTable user);
@Timestamped
@GetGeneratedKeys
@SqlUpdate("""
INSERT INTO users (uuid, created_at, name, email, tagline, read_prompts, locked, language, theme, email_verified, socials)
VALUES (:uuid, :now, :name, :email, :tagline, :readPrompts, :locked, :language, :theme, :emailVerified, :socials)
INSERT INTO users (uuid, created_at, name, email, tagline, read_prompts, locked, language, theme, email_verified, avatar_url, socials)
VALUES (:uuid, :now, :name, :email, :tagline, :readPrompts, :locked, :language, :theme, :emailVerified, :avatarUrl, :socials)
""")
UserTable create(UUID uuid, String name, String email, String tagline, String language, List<Integer> readPrompts, boolean locked, String theme, boolean emailVerified, JSONB socials);
UserTable create(UUID uuid, String name, String email, String tagline, String language, List<Integer> readPrompts, boolean locked, String theme, boolean emailVerified, String avatarUrl, JSONB socials);
@SqlUpdate("DELETE FROM users WHERE id = :id")
void delete(long id);
@GetGeneratedKeys
@SqlUpdate("UPDATE users SET name = :name, email = :email, tagline = :tagline, read_prompts = :readPrompts, locked = :locked, language = :language, theme = :theme, email_verified = :emailVerified, socials = :socials WHERE id = :id")
@SqlUpdate("UPDATE users SET name = :name, email = :email, tagline = :tagline, read_prompts = :readPrompts, locked = :locked, language = :language, theme = :theme, email_verified = :emailVerified, avatar_url = :avatarUrl, socials = :socials WHERE id = :id")
UserTable update(@BindBean UserTable user);
@SqlUpdate("DELETE FROM users WHERE id = :id")

View File

@ -60,7 +60,7 @@ public interface OrganizationRolesDAO extends IRolesDAO<OrganizationRoleTable> {
@KeyColumn("name")
@SqlQuery("""
SELECT o.name, uor.*, ow.id AS ownerId, ow.name AS ownerName, ou.uuid
SELECT o.name, uor.*, ow.id AS ownerId, ow.name AS ownerName, ou.uuid, ou.avatar_url
FROM user_organization_roles uor
JOIN organizations o ON o.id = uor.organization_id
JOIN users u ON uor.user_id = u.id

View File

@ -155,6 +155,7 @@ public interface ProjectsApiDAO {
" u.tagline," +
" u.locked," +
" u.socials, " +
" u.avatar_url," +
" array_agg(r.id) AS roles," +
" (SELECT count(*)" +
" FROM project_members_all pma" +
@ -182,6 +183,7 @@ public interface ProjectsApiDAO {
" u.tagline," +
" u.locked," +
" u.socials, " +
" u.avatar_url," +
" array_agg(r.id) AS roles," +
" (SELECT count(*)" +
" FROM project_members_all pma" +

View File

@ -109,6 +109,7 @@ public interface UsersApiDAO {
" u.tagline," +
" u.locked," +
" u.socials, " +
" u.avatar_url," +
" array(SELECT r.id FROM roles r JOIN user_global_roles ugr ON r.id = ugr.role_id WHERE u.id = ugr.user_id ORDER BY r.permission::bigint DESC) AS roles," +
" (SELECT count(*) FROM project_members_all pma WHERE pma.user_id = u.id) AS project_count" +
" FROM users u" +
@ -138,6 +139,7 @@ public interface UsersApiDAO {
" u.tagline," +
" u.locked," +
" u.socials, " +
" u.avatar_url," +
" array_agg(r.id ORDER BY r.permission::bigint DESC) AS roles," +
" (SELECT count(*) FROM project_members_all pma WHERE pma.user_id = u.id) AS project_count" +
" FROM users u" +

View File

@ -26,7 +26,7 @@ public class User extends Model implements Named, Identified {
private final JSONB socials;
@JdbiConstructor
public User(final OffsetDateTime createdAt, final long id, final String name, final String tagline, final List<Long> roles, final long projectCount, final boolean locked, @Nullable final List<UserNameChange> nameHistory, final JSONB socials) {
public User(final OffsetDateTime createdAt, final long id, final String name, final String tagline, final List<Long> roles, final long projectCount, final boolean locked, @Nullable final List<UserNameChange> nameHistory, final String avatarUrl, final JSONB socials) {
super(createdAt);
this.id = id;
this.name = name;
@ -36,6 +36,7 @@ public class User extends Model implements Named, Identified {
this.isOrganization = roles.contains(GlobalRole.ORGANIZATION.getRoleId());
this.locked = locked;
this.nameHistory = nameHistory;
this.avatarUrl = avatarUrl;
this.socials = socials;
}

View File

@ -85,6 +85,6 @@ public enum OrganizationRole implements Role<OrganizationRoleTable> {
@Override
public @NotNull OrganizationRoleTable create(final Long organizationId, final UUID uuid, final long userId, final boolean isAccepted) {
Preconditions.checkNotNull(organizationId, "organization id");
return new OrganizationRoleTable(userId, this, isAccepted, organizationId, uuid);
return new OrganizationRoleTable(userId, this, isAccepted, organizationId, uuid, null);
}
}

View File

@ -21,10 +21,11 @@ public class UserTable extends Table implements ProjectOwner {
private String language;
private String theme;
private boolean emailVerified;
private String avatarUrl;
private JSONB socials;
@JdbiConstructor
public UserTable(final OffsetDateTime createdAt, @PropagateNull final long id, final UUID uuid, final String name, final String email, final String tagline, final List<Integer> readPrompts, final boolean locked, final String language, final String theme, final boolean emailVerified, final JSONB socials) {
public UserTable(final OffsetDateTime createdAt, @PropagateNull final long id, final UUID uuid, final String name, final String email, final String tagline, final List<Integer> readPrompts, final boolean locked, final String language, final String theme, final boolean emailVerified, final String avatarUrl, final JSONB socials) {
super(createdAt, id);
this.uuid = uuid;
this.name = name;
@ -35,10 +36,11 @@ public class UserTable extends Table implements ProjectOwner {
this.language = language;
this.theme = theme;
this.emailVerified = emailVerified;
this.avatarUrl = avatarUrl;
this.socials = socials;
}
public UserTable(final long id, final UUID uuid, final String name, final String email, final List<Integer> readPrompts, final boolean locked, final String language, final String theme, final boolean emailVerified, final JSONB socials) {
public UserTable(final long id, final UUID uuid, final String name, final String email, final List<Integer> readPrompts, final boolean locked, final String language, final String theme, final boolean emailVerified, final String avatarUrl, final JSONB socials) {
super(id);
this.uuid = uuid;
this.name = name;
@ -48,6 +50,7 @@ public class UserTable extends Table implements ProjectOwner {
this.language = language;
this.theme = theme;
this.emailVerified = emailVerified;
this.avatarUrl = avatarUrl;
this.socials = socials;
}
@ -133,6 +136,14 @@ public class UserTable extends Table implements ProjectOwner {
this.socials = socials;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(final String avatarUrl) {
this.avatarUrl = avatarUrl;
}
@Override
public long getUserId() {
return this.id;

View File

@ -17,21 +17,23 @@ public class OrganizationRoleTable extends ExtendedRoleTable<OrganizationRole, O
private final UUID uuid;
private final long ownerId;
private final String ownerName;
private String avatarUrl;
private final String avatarUrl;
@JdbiConstructor
public OrganizationRoleTable(final OffsetDateTime createdAt, final long id, final long userId, @ColumnName("role_type") final OrganizationRole role, final boolean accepted, final long organizationId, @Nullable final UUID uuid, @Nullable final Long ownerId, @Nullable final String ownerName) {
public OrganizationRoleTable(final OffsetDateTime createdAt, final long id, final long userId, @ColumnName("role_type") final OrganizationRole role, final boolean accepted, final long organizationId, @Nullable final UUID uuid, @Nullable final Long ownerId, @Nullable final String ownerName, @Nullable final String avatarUrl) {
super(createdAt, id, userId, role, accepted);
this.organizationId = organizationId;
this.uuid = uuid;
this.ownerId = ownerId == null ? -1 : ownerId;
this.ownerName = ownerName;
this.avatarUrl = avatarUrl;
}
public OrganizationRoleTable(final long userId, final OrganizationRole role, final boolean accepted, final long organizationId, @Nullable final UUID uuid) {
public OrganizationRoleTable(final long userId, final OrganizationRole role, final boolean accepted, final long organizationId, @Nullable final UUID uuid, @Nullable final String avatarUrl) {
super(userId, role, accepted);
this.organizationId = organizationId;
this.uuid = uuid;
this.avatarUrl = avatarUrl;
this.ownerId = -1;
this.ownerName = null;
}
@ -63,10 +65,6 @@ public class OrganizationRoleTable extends ExtendedRoleTable<OrganizationRole, O
return this.avatarUrl;
}
public void setAvatarUrl(final String avatarUrl) {
this.avatarUrl = avatarUrl;
}
@Override
public String toString() {
return "OrganizationRoleTable{" +

View File

@ -25,8 +25,8 @@ public class HangarUser extends User {
@Nullable
private Integer aal;
public HangarUser(final OffsetDateTime createdAt, final String name, final String tagline, final List<Long> roles, final long projectCount, final boolean locked, @Nullable final List<UserNameChange> nameHistory, final long id, final UUID uuid, final String email, final List<Integer> readPrompts, final String language, final String theme, final JSONB socials) {
super(createdAt, id, name, tagline, roles, projectCount, locked, nameHistory, socials);
public HangarUser(final OffsetDateTime createdAt, final String name, final String tagline, final List<Long> roles, final long projectCount, final boolean locked, @Nullable final List<UserNameChange> nameHistory, final long id, final UUID uuid, final String email, final List<Integer> readPrompts, final String language, final String theme, final String avatarUrl, final JSONB socials) {
super(createdAt, id, name, tagline, roles, projectCount, locked, nameHistory, avatarUrl, socials);
this.uuid = uuid;
this.email = email;
this.readPrompts = readPrompts;

View File

@ -9,7 +9,6 @@ public class JoinableMember<R extends ExtendedRoleTable<?, ?>> {
private final R role;
private final UserTable user;
private final boolean hidden;
private String avatarUrl;
public JoinableMember(@Nested final R role, final UserTable user, final boolean hidden) {
this.role = role;
@ -29,14 +28,6 @@ public class JoinableMember<R extends ExtendedRoleTable<?, ?>> {
return this.hidden;
}
public String getAvatarUrl() {
return this.avatarUrl;
}
public void setAvatarUrl(final String avatarUrl) {
this.avatarUrl = avatarUrl;
}
@Override
public String toString() {
return "JoinableMember{" +

View File

@ -70,16 +70,12 @@ public class ProjectsApiService extends HangarComponent {
@Transactional(readOnly = true)
public PaginatedResult<User> getProjectStargazers(final ProjectTable project, final RequestPagination pagination) {
final List<User> stargazers = this.projectsApiDAO.getProjectStargazers(project.getId(), pagination.getLimit(), pagination.getOffset());
// TODO rewrite avatar fetching
stargazers.forEach(this.usersApiService::supplyAvatarUrl);
return new PaginatedResult<>(new Pagination(this.projectsApiDAO.getProjectStargazersCount(project.getId()), pagination), stargazers);
}
@Transactional(readOnly = true)
public PaginatedResult<User> getProjectWatchers(final ProjectTable project, final RequestPagination pagination) {
final List<User> watchers = this.projectsApiDAO.getProjectWatchers(project.getId(), pagination.getLimit(), pagination.getOffset());
// TODO rewrite avatar fetching
watchers.forEach(this.usersApiService::supplyAvatarUrl);
return new PaginatedResult<>(new Pagination(this.projectsApiDAO.getProjectWatchersCount(project.getId()), pagination), watchers);
}

View File

@ -70,8 +70,6 @@ public class UsersApiService extends HangarComponent {
public <T extends User> T getUser(final long id, final Class<T> type) {
final T user = this.getUserRequired(id, this.usersDAO::getUser, type);
this.supplyNameHistory(user);
// TODO rewrite avatar fetching
this.supplyAvatarUrl(user);
return user instanceof HangarUser ? (T) this.supplyHeaderData((HangarUser) user) : user;
}
@ -79,8 +77,6 @@ public class UsersApiService extends HangarComponent {
public <T extends User> PaginatedResult<T> getUsers(final String query, final RequestPagination pagination, final Class<T> type) {
final boolean hasQuery = !StringUtils.isBlank(query);
final List<T> users = this.usersDAO.getUsers(hasQuery, query, pagination, type);
// TODO rewrite avatar fetching
users.forEach(u -> u.setAvatarUrl(this.avatarService.getUserAvatarUrl(u)));
return new PaginatedResult<>(new Pagination(this.usersDAO.getUsersCount(hasQuery, query), pagination), users);
}
@ -112,8 +108,6 @@ public class UsersApiService extends HangarComponent {
public PaginatedResult<User> getAuthors(final String query, final RequestPagination pagination) {
final boolean hasQuery = !StringUtils.isBlank(query);
final List<User> users = this.usersApiDAO.getAuthors(hasQuery, query, pagination);
// TODO rewrite avatar fetching (less important)
users.forEach(u -> u.setAvatarUrl(this.avatarService.getUserAvatarUrl(u)));
final long count = this.usersApiDAO.getAuthorsCount(hasQuery, query);
return new PaginatedResult<>(new Pagination(count, pagination), users);
}
@ -128,8 +122,6 @@ public class UsersApiService extends HangarComponent {
public PaginatedResult<User> getStaff(final String query, final RequestPagination pagination) {
final boolean hasQuery = !StringUtils.isBlank(query);
final List<User> users = this.usersApiDAO.getStaff(hasQuery, query, this.config.user.staffRoles(), pagination);
// TODO rewrite avatar fetching (less important)
users.forEach(u -> u.setAvatarUrl(this.avatarService.getUserAvatarUrl(u)));
final long count = this.usersApiDAO.getStaffCount(hasQuery, query, this.config.user.staffRoles());
return new PaginatedResult<>(new Pagination(count, pagination), users);
}
@ -175,11 +167,6 @@ public class UsersApiService extends HangarComponent {
user.setNameHistory(userNameHistory);
}
@Deprecated(forRemoval = true)
public void supplyAvatarUrl(final User user) {
user.setAvatarUrl(this.avatarService.getUserAvatarUrl(user));
}
public List<ProjectCompact> getUserPinned(final UserTable user) {
return this.pinnedProjectService.getPinnedProjects(user.getId());
}

View File

@ -5,6 +5,7 @@ import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.components.auth.model.credential.PasswordCredential;
import io.papermc.hangar.components.auth.model.credential.TotpCredential;
import io.papermc.hangar.components.auth.service.CredentialsService;
import io.papermc.hangar.components.images.service.AvatarService;
import io.papermc.hangar.config.CacheConfig;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.dao.internal.table.UserDAO;
@ -52,8 +53,9 @@ public class FakeDataService extends HangarComponent {
private final CredentialsService credentialsService;
private final PasswordEncoder passwordEncoder;
private final QrDataFactory qrDataFactory;
private final AvatarService avatarService;
public FakeDataService(final UserDAO userDAO, final GlobalRoleService globalRoleService, final ProjectService projectService, final ProjectFactory projectFactory, final ProjectsDAO projectsDAO, final CredentialsService credentialsService, final PasswordEncoder passwordEncoder, final QrDataFactory qrDataFactory) {
public FakeDataService(final UserDAO userDAO, final GlobalRoleService globalRoleService, final ProjectService projectService, final ProjectFactory projectFactory, final ProjectsDAO projectsDAO, final CredentialsService credentialsService, final PasswordEncoder passwordEncoder, final QrDataFactory qrDataFactory, final AvatarService avatarService) {
this.userDAO = userDAO;
this.globalRoleService = globalRoleService;
this.projectService = projectService;
@ -62,6 +64,7 @@ public class FakeDataService extends HangarComponent {
this.credentialsService = credentialsService;
this.passwordEncoder = passwordEncoder;
this.qrDataFactory = qrDataFactory;
this.avatarService = avatarService;
}
@CacheEvict(cacheNames = CacheConfig.PROJECTS, allEntries = true)
@ -101,6 +104,7 @@ public class FakeDataService extends HangarComponent {
false,
"dark",
true,
this.avatarService.getDefaultAvatarUrl(),
new JSONB(Map.of()));
this.globalRoleService.addRole(new GlobalRoleTable(userTable.getId(), GlobalRole.DUMMY));
return userTable;
@ -158,6 +162,7 @@ public class FakeDataService extends HangarComponent {
false,
"dark",
true,
this.avatarService.getDefaultAvatarUrl(),
new JSONB(Map.of()));
this.globalRoleService.addRole(new GlobalRoleTable(admin.getId(), GlobalRole.HANGAR_ADMIN));
@ -169,6 +174,7 @@ public class FakeDataService extends HangarComponent {
false,
"dark",
true,
this.avatarService.getDefaultAvatarUrl(),
new JSONB(Map.of()));
final String password = this.config.e2e.password();

View File

@ -1,6 +1,7 @@
package io.papermc.hangar.service.internal.organizations;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.components.images.service.AvatarService;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.dao.internal.table.OrganizationDAO;
import io.papermc.hangar.db.dao.internal.table.UserDAO;
@ -34,9 +35,10 @@ public class OrganizationFactory extends HangarComponent {
private final ProjectInviteService inviteService;
private final ProjectsDAO projectsDAO;
private final ProjectFactory projectFactory;
private final AvatarService avatarService;
@Autowired
public OrganizationFactory(final UserDAO userDAO, final OrganizationDAO organizationDAO, final OrganizationService organizationService, final OrganizationMemberService organizationMemberService, final GlobalRoleService globalRoleService, final ProjectInviteService inviteService, final ProjectsDAO projectsDAO, final ProjectFactory projectFactory) {
public OrganizationFactory(final UserDAO userDAO, final OrganizationDAO organizationDAO, final OrganizationService organizationService, final OrganizationMemberService organizationMemberService, final GlobalRoleService globalRoleService, final ProjectInviteService inviteService, final ProjectsDAO projectsDAO, final ProjectFactory projectFactory, final AvatarService avatarService) {
this.userDAO = userDAO;
this.organizationDAO = organizationDAO;
this.organizationService = organizationService;
@ -45,6 +47,7 @@ public class OrganizationFactory extends HangarComponent {
this.inviteService = inviteService;
this.projectsDAO = projectsDAO;
this.projectFactory = projectFactory;
this.avatarService = avatarService;
}
@Transactional
@ -57,7 +60,7 @@ public class OrganizationFactory extends HangarComponent {
}
final String dummyEmail = name.replaceAll("[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]", "") + '@' + this.config.org.dummyEmailDomain();
final UserTable userTable = this.userDAO.create(UUID.randomUUID(), name, dummyEmail, "", "", List.of(), false, null, true, new JSONB(Map.of()));
final UserTable userTable = this.userDAO.create(UUID.randomUUID(), name, dummyEmail, "", "", List.of(), false, null, true, this.avatarService.getDefaultAvatarUrl(), new JSONB(Map.of()));
final OrganizationTable organizationTable = this.organizationDAO.insert(new OrganizationTable(userTable.getId(), name, this.getHangarPrincipal().getId(), userTable.getId(), userTable.getUuid()));
this.globalRoleService.addRole(GlobalRole.ORGANIZATION.create(null, userTable.getUuid(), userTable.getId(), false));
this.organizationMemberService.addNewAcceptedByDefaultMember(OrganizationRole.ORGANIZATION_OWNER.create(organizationTable.getId(), userTable.getUuid(), this.getHangarPrincipal().getId(), true));

View File

@ -75,8 +75,6 @@ public class OrganizationService extends HangarComponent {
}
final UserTable owner = this.userDAO.getUserTable(organizationTable.getOwnerId());
final List<JoinableMember<OrganizationRoleTable>> members = this.hangarOrganizationsDAO.getOrganizationMembers(organizationTable.getId(), this.getHangarUserId(), this.permissionService.getOrganizationPermissions(this.getHangarUserId(), organizationTable.getId()).has(Permission.ManageOrganizationMembers));
// TODO rewrite avatar fetching
members.forEach(member -> member.setAvatarUrl(this.avatarService.getUserAvatarUrl(member.getUser())));
return new HangarOrganization(organizationTable.getId(), owner, members);
}
@ -86,8 +84,6 @@ public class OrganizationService extends HangarComponent {
final Map<String, Boolean> visibility = this.organizationMemberService.getUserOrganizationMembershipVisibility(user);
roles.keySet().removeIf(org -> Boolean.TRUE.equals(visibility.getOrDefault(org, true)));
}
// TODO rewrite avatar fetching
roles.values().forEach(org -> org.setAvatarUrl(this.avatarService.getUserAvatarUrl(org.getUuid())));
return roles;
}

View File

@ -145,10 +145,7 @@ public class ProjectService extends HangarComponent {
}
final CompletableFuture<List<JoinableMember<ProjectRoleTable>>> membersFuture = this.supply(() -> {
final List<JoinableMember<ProjectRoleTable>> members = this.hangarProjectsDAO.getProjectMembers(projectId, hangarUserId, this.permissionService.getProjectPermissions(hangarUserId, projectId).has(Permission.EditProjectSettings));
// TODO rewrite avatar fetching
members.parallelStream().forEach((member) -> member.setAvatarUrl(this.avatarService.getUserAvatarUrl(member.getUser())));
return members;
return this.hangarProjectsDAO.getProjectMembers(projectId, hangarUserId, this.permissionService.getProjectPermissions(hangarUserId, projectId).has(Permission.EditProjectSettings));
});
final Map<Platform, HangarVersion> mainChannelVersions = new EnumMap<>(Platform.class);

View File

@ -1,5 +1,6 @@
package io.papermc.hangar.service.internal.versions;
import io.papermc.hangar.components.images.service.AvatarService;
import io.papermc.hangar.db.customtypes.JSONB;
import io.papermc.hangar.db.dao.internal.table.projects.ProjectsDAO;
import io.papermc.hangar.db.dao.internal.table.versions.ProjectVersionsDAO;
@ -62,9 +63,10 @@ public class JarScanningService {
private final FileService fileService;
private final ReviewService reviewService;
private final UserService userService;
private final AvatarService avatarService;
private UserTable jarScannerUser;
public JarScanningService(final JarScanResultDAO dao, final ProjectVersionsDAO projectVersionsDAO, final ProjectVersionDownloadsDAO downloadsDAO, final ProjectsDAO projectsDAO, final ProjectFiles projectFiles, final FileService fileService, final ReviewService reviewService, final UserService userService) {
public JarScanningService(final JarScanResultDAO dao, final ProjectVersionsDAO projectVersionsDAO, final ProjectVersionDownloadsDAO downloadsDAO, final ProjectsDAO projectsDAO, final ProjectFiles projectFiles, final FileService fileService, final ReviewService reviewService, final UserService userService, final AvatarService avatarService) {
this.dao = dao;
this.projectVersionsDAO = projectVersionsDAO;
this.downloadsDAO = downloadsDAO;
@ -73,6 +75,7 @@ public class JarScanningService {
this.fileService = fileService;
this.reviewService = reviewService;
this.userService = userService;
this.avatarService = avatarService;
}
@EventListener(ApplicationReadyEvent.class)
@ -112,6 +115,7 @@ public class JarScanningService {
Locale.ENGLISH.toLanguageTag(),
"white",
true,
this.avatarService.getDefaultAvatarUrl(),
new JSONB(Map.of())
);
return this.userService.insertUser(userTable);

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255);

View File

@ -118,9 +118,16 @@ async function save() {
window.location.reload();
} catch (err) {
notifications.error("Error while saving avatar");
console.error("Error while saving avatar", err);
handleRequestError(err, "Error while saving avatar");
reset();
modal.value?.close();
}
}
function reset() {
cropperResult.value = undefined;
selectedFile.value = undefined;
cropperInput.value = undefined;
v.value.$reset();
}
</script>

View File

@ -160,7 +160,7 @@ async function doSearch(val?: string) {
:key="member.user.name"
class="p-2 w-full border border-gray-100 dark:border-gray-800 rounded inline-flex flex-row space-x-4"
>
<UserAvatar :username="member.user.name" :avatar-url="member.avatarUrl" size="sm" class="flex-shrink-0" />
<UserAvatar :username="member.user.name" :avatar-url="member.user.avatarUrl" size="sm" class="flex-shrink-0" />
<div class="flex-grow truncate">
<p class="font-semibold">
<Link :to="'/' + member.user.name">{{ member.user.name }}</Link>

View File

@ -769,6 +769,7 @@ export interface UserTable {
language: string;
theme: string;
emailVerified: boolean;
avatarUrl: string;
socials: JsonNode;
/** @format int64 */
userId: number;
@ -1180,7 +1181,6 @@ export interface JoinableMemberProjectRoleTable {
role: ProjectRoleTable;
user: UserTable;
hidden: boolean;
avatarUrl: string;
}
export interface PossibleProjectOwner {
@ -1239,7 +1239,6 @@ export interface JoinableMemberOrganizationRoleTable {
role: OrganizationRoleTable;
user: UserTable;
hidden: boolean;
avatarUrl: string;
}
export enum OAuthMode {