Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated code lifecycle: Support multiple SSH keys per user #9478

Merged
merged 106 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from 103 commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
3786b60
added new table to add multiple keys for users
SimonEntholzer Oct 14, 2024
4922371
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 14, 2024
94f39c3
enable cloning
SimonEntholzer Oct 15, 2024
af8f80b
fixed css
SimonEntholzer Oct 17, 2024
366cbaf
revert debug change
SimonEntholzer Oct 17, 2024
6e4c8ac
added migration mysql
SimonEntholzer Oct 17, 2024
bded252
added migration for postgres
SimonEntholzer Oct 17, 2024
002de21
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 17, 2024
5b7bb96
cleaned up UI
SimonEntholzer Oct 18, 2024
7f10abe
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 20, 2024
5805139
fixed build agent authentication
SimonEntholzer Oct 20, 2024
a186891
fixed authentication in tests
SimonEntholzer Oct 20, 2024
b4e75a5
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 20, 2024
74b4a32
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 21, 2024
4b3c308
finish UI
SimonEntholzer Oct 21, 2024
9616e2d
added javadocs
SimonEntholzer Oct 21, 2024
72e6ec9
more docs
SimonEntholzer Oct 21, 2024
06d87ff
server tests
SimonEntholzer Oct 21, 2024
cbec60e
fix server style
SimonEntholzer Oct 22, 2024
f04d01e
moved deleteall
SimonEntholzer Oct 22, 2024
a61e360
add code rabbit suggestions
SimonEntholzer Oct 22, 2024
6a704e2
let code button detect if a user has ssh keys or not
SimonEntholzer Oct 22, 2024
11994ee
improve Server test coverage
SimonEntholzer Oct 22, 2024
f4cde1b
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 22, 2024
e0a01c4
added date picker
SimonEntholzer Oct 22, 2024
fb2b67a
cleaned up client code and added tests
SimonEntholzer Oct 22, 2024
75e3a0b
Use DTO instead of database entity
SimonEntholzer Oct 22, 2024
52c6b3b
moved DTO
SimonEntholzer Oct 22, 2024
c90d4e7
add change suggestions
SimonEntholzer Oct 22, 2024
73e147a
remove debugging statement
SimonEntholzer Oct 22, 2024
71ef085
fix client test style
SimonEntholzer Oct 23, 2024
54c64f1
Merge remote-tracking branch 'refs/remotes/origin/develop' into featu…
SimonEntholzer Oct 23, 2024
7d92399
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 24, 2024
8f96960
replaced put with post
SimonEntholzer Oct 24, 2024
abc1f0f
remove button
SimonEntholzer Oct 24, 2024
8ab97e3
fix copy paste error
SimonEntholzer Oct 24, 2024
9aa50d5
fix tests
SimonEntholzer Oct 24, 2024
7de1858
fix typo
SimonEntholzer Oct 24, 2024
e8ac909
improved client coverage
SimonEntholzer Oct 24, 2024
2079800
import correct dayjs
SimonEntholzer Oct 24, 2024
2c7e18e
remove unused translation keys
SimonEntholzer Oct 25, 2024
c29d5d3
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 25, 2024
3f179a4
added suggestions
SimonEntholzer Oct 26, 2024
6807a32
handle case when user inputs too long label
SimonEntholzer Oct 26, 2024
39b1458
improve error handling when deleting key
SimonEntholzer Oct 26, 2024
1038cf2
add code rabbit suggestions
SimonEntholzer Oct 26, 2024
3b1a0db
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 26, 2024
44b33ec
add code rabbit suggestions
SimonEntholzer Oct 26, 2024
0219251
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 28, 2024
6086881
fix expected http error code in test
SimonEntholzer Oct 28, 2024
56b7d53
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 28, 2024
8e4a98b
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 28, 2024
c92dc8f
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 29, 2024
46a755c
Update src/main/java/de/tum/cit/aet/artemis/programming/service/UserS…
SimonEntholzer Oct 29, 2024
4d521ef
add code suggestions
SimonEntholzer Oct 29, 2024
d5ca308
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 30, 2024
e06bf0b
remove empty line from master.xml from merge conflict
SimonEntholzer Oct 30, 2024
2fcd95c
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 1, 2024
1121dab
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
krusche Nov 2, 2024
419aa5f
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
krusche Nov 3, 2024
e110f36
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 4, 2024
0275455
refactored into separate service, improved migration and reduced code…
SimonEntholzer Nov 5, 2024
2b507c6
simplify back navigation
SimonEntholzer Nov 5, 2024
8cd6402
fix tests
SimonEntholzer Nov 6, 2024
0f3e26c
improve default key labeling
SimonEntholzer Nov 6, 2024
8584ccf
fix race condition by making sure the user is loaded
SimonEntholzer Nov 7, 2024
73e6a24
re-add useSSH
SimonEntholzer Nov 7, 2024
e7ada88
removed possible race conditions
SimonEntholzer Nov 7, 2024
1e1c80e
removed another race condition
SimonEntholzer Nov 7, 2024
c4e6ad1
Merge branch 'develop' into bugfix/programming/code-button-race-condi…
SimonEntholzer Nov 7, 2024
eed3303
Merge branch 'develop' into bugfix/programming/code-button-race-condi…
SimonEntholzer Nov 9, 2024
1b7efe9
fix tests
SimonEntholzer Nov 9, 2024
099397b
add error message when key is expired
SimonEntholzer Nov 9, 2024
36d9a61
added test
SimonEntholzer Nov 9, 2024
071af6b
Merge branch 'refs/heads/bugfix/programming/code-button-race-conditio…
SimonEntholzer Nov 9, 2024
4a57175
fix tests and remove console log
SimonEntholzer Nov 9, 2024
4857977
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 9, 2024
89a8bf9
Merge remote-tracking branch 'refs/remotes/origin/develop' into featu…
SimonEntholzer Nov 10, 2024
6032f28
made SSH uppercase
SimonEntholzer Nov 10, 2024
db0b53d
fixed java comments and RequestMapping '/'
SimonEntholzer Nov 10, 2024
e5021bf
fix tests
SimonEntholzer Nov 10, 2024
b0a1e13
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 11, 2024
d143f83
changed test
SimonEntholzer Nov 10, 2024
79f649a
fix violation error
SimonEntholzer Nov 11, 2024
2d1b025
fix typo
SimonEntholzer Nov 11, 2024
82620de
fix client tests
SimonEntholzer Nov 11, 2024
cad81a7
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 11, 2024
afb0e55
fix tests
SimonEntholzer Nov 11, 2024
43f8a85
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 12, 2024
986d446
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 12, 2024
322d4d8
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 13, 2024
0ec6548
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 14, 2024
5255f5d
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 15, 2024
1a9152a
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 16, 2024
65d8aa8
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 16, 2024
d471e9f
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 16, 2024
c0cefce
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 16, 2024
4fa1192
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 17, 2024
f14a367
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 19, 2024
17c7cf0
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 22, 2024
1ee35c8
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 25, 2024
1a5074e
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 26, 2024
8408ab7
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 26, 2024
f69a283
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 27, 2024
f8b8709
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Nov 28, 2024
ade905b
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
krusche Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions src/main/java/de/tum/cit/aet/artemis/core/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,23 +155,6 @@ public class User extends AbstractAuditingEntity implements Participant {
@Column(name = "vcs_access_token_expiry_date")
private ZonedDateTime vcsAccessTokenExpiryDate = null;

/**
* The actual full public ssh key of a user used to authenticate git clone and git push operations if available
*/
@Nullable
@JsonIgnore
@Column(name = "ssh_public_key")
private final String sshPublicKey = null;

/**
* A hash of the public ssh key for fast comparison in the database (with an index)
*/
@Nullable
@Size(max = 100)
@JsonIgnore
@Column(name = "ssh_public_key_hash")
private final String sshPublicKeyHash = null;

@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "user_groups", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "user_groups")
Expand Down Expand Up @@ -556,14 +539,4 @@ public void hasAcceptedIrisElseThrow() {
throw new AccessForbiddenException("The user has not accepted the Iris privacy policy yet.");
}
}

@Nullable
public String getSshPublicKey() {
return sshPublicKey;
}

@Nullable
public @Size(max = 100) String getSshPublicKeyHash() {
return sshPublicKeyHash;
}
}
20 changes: 0 additions & 20 deletions src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ public class UserDTO extends AuditingEntityDTO {

private ZonedDateTime vcsAccessTokenExpiryDate;

private String sshPublicKey;

private String sshKeyHash;

private ZonedDateTime irisAccepted;

public UserDTO() {
Expand Down Expand Up @@ -262,14 +258,6 @@ public ZonedDateTime getVcsAccessTokenExpiryDate() {
return vcsAccessTokenExpiryDate;
}

public String getSshPublicKey() {
return sshPublicKey;
}

public void setSshPublicKey(String sshPublicKey) {
this.sshPublicKey = sshPublicKey;
}

@Override
public String toString() {
return "UserDTO{" + "login='" + login + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + ", imageUrl='"
Expand All @@ -293,12 +281,4 @@ public ZonedDateTime getIrisAccepted() {
public void setIrisAccepted(ZonedDateTime irisAccepted) {
this.irisAccepted = irisAccepted;
}

public String getSshKeyHash() {
return sshKeyHash;
}

public void setSshKeyHash(String sshKeyHash) {
this.sshKeyHash = sshKeyHash;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -724,16 +724,6 @@ default Page<User> searchAllWithGroupsByLoginOrNameInCourseAndReturnPage(Pageabl
""")
void updateUserLanguageKey(@Param("userId") long userId, @Param("languageKey") String languageKey);

@Modifying
@Transactional // ok because of modifying query
@Query("""
UPDATE User user
SET user.sshPublicKeyHash = :sshPublicKeyHash,
user.sshPublicKey = :sshPublicKey
WHERE user.id = :userId
""")
void updateUserSshPublicKeyHash(@Param("userId") long userId, @Param("sshPublicKeyHash") String sshPublicKeyHash, @Param("sshPublicKey") String sshPublicKey);

@Modifying
@Transactional // ok because of modifying query
@Query("""
Expand Down Expand Up @@ -1120,8 +1110,6 @@ default boolean isCurrentUser(String login) {
return SecurityUtils.getCurrentUserLogin().map(currentLogin -> currentLogin.equals(login)).orElse(false);
}

Optional<User> findBySshPublicKeyHash(String keyString);

/**
* Finds all users which a non-null VCS access token that expires before some given date.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.time.ZonedDateTime;
import java.util.Optional;

import jakarta.validation.Valid;
import jakarta.ws.rs.BadRequestException;

import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
Expand Down Expand Up @@ -45,7 +41,6 @@
import de.tum.cit.aet.artemis.core.service.user.UserCreationService;
import de.tum.cit.aet.artemis.core.service.user.UserService;
import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCPersonalAccessTokenManagementService;
import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils;

/**
* REST controller for managing the current user's account.
Expand Down Expand Up @@ -128,50 +123,6 @@ public ResponseEntity<Void> changePassword(@RequestBody PasswordChangeDTO passwo
return ResponseEntity.ok().build();
}

/**
* PUT account/ssh-public-key : sets the ssh public key
*
* @param sshPublicKey the ssh public key to set
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@PutMapping("account/ssh-public-key")
@EnforceAtLeastStudent
public ResponseEntity<Void> addSshPublicKey(@RequestBody String sshPublicKey) throws GeneralSecurityException, IOException {

User user = userRepository.getUser();
log.debug("REST request to add SSH key to user {}", user.getLogin());
// Parse the public key string
AuthorizedKeyEntry keyEntry;
try {
keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshPublicKey);
}
catch (IllegalArgumentException e) {
throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true);
}
// Extract the PublicKey object
PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null);
String keyHash = HashUtils.getSha512Fingerprint(publicKey);
userRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey);
return ResponseEntity.ok().build();
}

/**
* PUT account/ssh-public-key : sets the ssh public key
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@DeleteMapping("account/ssh-public-key")
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteSshPublicKey() {
User user = userRepository.getUser();
log.debug("REST request to remove SSH key of user {}", user.getLogin());
userRepository.updateUserSshPublicKeyHash(user.getId(), null, null);

log.debug("Successfully deleted SSH key of user {}", user.getLogin());
return ResponseEntity.ok().build();
}

/**
* PUT account/user-vcs-access-token : creates a vcsAccessToken for a user
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ public ResponseEntity<UserDTO> getAccount() {
// we set this value on purpose here: the user can only fetch their own information, make the token available for constructing the token-based clone-URL
userDTO.setVcsAccessToken(user.getVcsAccessToken());
userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate());
userDTO.setSshPublicKey(user.getSshPublicKey());
userDTO.setSshKeyHash(user.getSshPublicKeyHash());
log.info("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start);
return ResponseEntity.ok(userDTO);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package de.tum.cit.aet.artemis.programming.domain;

import java.time.ZonedDateTime;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import de.tum.cit.aet.artemis.core.domain.DomainObject;

/**
* A public SSH key of a user.
*/
@Entity
@Table(name = "user_public_ssh_key")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class UserSshPublicKey extends DomainObject {
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* The user who is owner of the public key
*/
@NotNull
@Column(name = "user_id")
private long userId;

/**
* The label of the SSH key shwon in the UI
*/
@Size(max = 50)
@Column(name = "label", length = 50)
private String label;

/**
* The actual full public ssh key of a user used to authenticate git clone and git push operations if available
*/
@NotNull
@Column(name = "public_key")
private String publicKey;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* A hash of the public ssh key for fast comparison in the database (with an index)
*/
@Size(max = 100)
@Column(name = "key_hash")
private String keyHash;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* The creation date of the public SSH key
*/
@Column(name = "creation_date")
private ZonedDateTime creationDate = null;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

/**
* The last used date of the public SSH key
*/
@Nullable
@Column(name = "last_used_date")
private ZonedDateTime lastUsedDate = null;

/**
* The expiry date of the public SSH key
*/
@Nullable
@Column(name = "expiry_date")
private ZonedDateTime expiryDate = null;

public @NotNull long getUserId() {
return userId;
}

public void setUserId(@NotNull long userId) {
this.userId = userId;
}

public @Size(max = 50) String getLabel() {
return label;
}

public void setLabel(@Size(max = 50) String label) {
this.label = label;
}

public String getPublicKey() {
return publicKey;
}

public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

@Nullable
public @Size(max = 100) String getKeyHash() {
return keyHash;
}

public void setKeyHash(@Nullable @Size(max = 100) String keyHash) {
this.keyHash = keyHash;
}

public ZonedDateTime getCreationDate() {
return creationDate;
}

public void setCreationDate(ZonedDateTime creationDate) {
this.creationDate = creationDate;
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

@Nullable
public ZonedDateTime getLastUsedDate() {
return lastUsedDate;
}

public void setLastUsedDate(@Nullable ZonedDateTime lastUsedDate) {
this.lastUsedDate = lastUsedDate;
}

@Nullable
public ZonedDateTime getExpiryDate() {
return expiryDate;
}

public void setExpiryDate(@Nullable ZonedDateTime expiryDate) {
this.expiryDate = expiryDate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.tum.cit.aet.artemis.programming.dto;

import java.time.ZonedDateTime;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record UserSshPublicKeyDTO(Long id, String label, String publicKey, String keyHash, ZonedDateTime creationDate, ZonedDateTime lastUsedDate, ZonedDateTime expiryDate) {
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved

public static UserSshPublicKeyDTO of(UserSshPublicKey userSshPublicKey) {
return new UserSshPublicKeyDTO(userSshPublicKey.getId(), userSshPublicKey.getLabel(), userSshPublicKey.getPublicKey(), userSshPublicKey.getKeyHash(),
userSshPublicKey.getCreationDate(), userSshPublicKey.getLastUsedDate(), userSshPublicKey.getExpiryDate());
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.tum.cit.aet.artemis.programming.repository;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.List;
import java.util.Optional;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;

import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey;

@Profile(PROFILE_CORE)
@Repository
public interface UserSshPublicKeyRepository extends ArtemisJpaRepository<UserSshPublicKey, Long> {

List<UserSshPublicKey> findAllByUserId(Long userId);

Optional<UserSshPublicKey> findByKeyHash(String keyHash);

Optional<UserSshPublicKey> findByIdAndUserId(Long keyId, Long userId);

boolean existsByIdAndUserId(Long id, Long userId);

boolean existsByUserId(Long userId);
}
Loading
Loading