feat(domain/infra/auth): add domain entities, JPA repos, and auth entities

Task 8: Domain layer
- UserAccount entity with UserStatus lifecycle
- Namespace, NamespaceMember entities with NamespaceRole
- Repository interfaces for domain aggregates

Task 9: Infra layer
- UserAccountJpaRepository, NamespaceJpaRepository, NamespaceMemberJpaRepository
- Spring Data JPA implementations

Task 10: Auth entities and repositories
- IdentityBinding, ApiToken, Role, Permission, RolePermission, UserRoleBinding
- JPA repositories for all auth entities
- UserRoleBinding with eager-loaded Role for RBAC queries
This commit is contained in:
vsxd 2026-03-11 23:42:53 +08:00
parent edfff6abc7
commit 2991eb11e0
23 changed files with 588 additions and 0 deletions

View file

@ -0,0 +1,77 @@
package com.iflytek.skillhub.auth.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "api_token")
public class ApiToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "subject_type", nullable = false, length = 32)
private String subjectType = "USER";
@Column(name = "subject_id", nullable = false)
private Long subjectId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(nullable = false, length = 128)
private String name;
@Column(name = "token_prefix", nullable = false, length = 16)
private String tokenPrefix;
@Column(name = "token_hash", nullable = false, unique = true, length = 64)
private String tokenHash;
@Column(name = "scope_json", nullable = false, columnDefinition = "jsonb")
private String scopeJson;
@Column(name = "expires_at")
private LocalDateTime expiresAt;
@Column(name = "last_used_at")
private LocalDateTime lastUsedAt;
@Column(name = "revoked_at")
private LocalDateTime revokedAt;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
protected ApiToken() {}
public ApiToken(Long userId, String name, String tokenPrefix, String tokenHash, String scopeJson) {
this.subjectType = "USER";
this.subjectId = userId;
this.userId = userId;
this.name = name;
this.tokenPrefix = tokenPrefix;
this.tokenHash = tokenHash;
this.scopeJson = scopeJson;
}
@PrePersist
void prePersist() { this.createdAt = LocalDateTime.now(); }
public Long getId() { return id; }
public Long getUserId() { return userId; }
public String getName() { return name; }
public String getTokenPrefix() { return tokenPrefix; }
public String getTokenHash() { return tokenHash; }
public String getScopeJson() { return scopeJson; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
public LocalDateTime getLastUsedAt() { return lastUsedAt; }
public void setLastUsedAt(LocalDateTime lastUsedAt) { this.lastUsedAt = lastUsedAt; }
public LocalDateTime getRevokedAt() { return revokedAt; }
public void setRevokedAt(LocalDateTime revokedAt) { this.revokedAt = revokedAt; }
public LocalDateTime getCreatedAt() { return createdAt; }
public boolean isRevoked() { return revokedAt != null; }
public boolean isExpired() { return expiresAt != null && expiresAt.isBefore(LocalDateTime.now()); }
public boolean isValid() { return !isRevoked() && !isExpired(); }
}

View file

@ -0,0 +1,66 @@
package com.iflytek.skillhub.auth.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "identity_binding",
uniqueConstraints = @UniqueConstraint(columnNames = {"provider_code", "subject"}))
public class IdentityBinding {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "provider_code", nullable = false, length = 64)
private String providerCode;
@Column(nullable = false, length = 256)
private String subject;
@Column(name = "login_name", length = 128)
private String loginName;
@Column(name = "extra_json", columnDefinition = "jsonb")
private String extraJson;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
protected IdentityBinding() {}
public IdentityBinding(Long userId, String providerCode, String subject, String loginName) {
this.userId = userId;
this.providerCode = providerCode;
this.subject = subject;
this.loginName = loginName;
}
@PrePersist
void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
}
@PreUpdate
void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getProviderCode() { return providerCode; }
public void setProviderCode(String providerCode) { this.providerCode = providerCode; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public String getLoginName() { return loginName; }
public void setLoginName(String loginName) { this.loginName = loginName; }
public String getExtraJson() { return extraJson; }
public void setExtraJson(String extraJson) { this.extraJson = extraJson; }
}

View file

@ -0,0 +1,24 @@
package com.iflytek.skillhub.auth.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "permission")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 128)
private String code;
@Column(nullable = false, length = 128)
private String name;
@Column(name = "group_code", length = 64)
private String groupCode;
public Long getId() { return id; }
public String getCode() { return code; }
public String getName() { return name; }
}

View file

@ -0,0 +1,35 @@
package com.iflytek.skillhub.auth.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 64)
private String code;
@Column(nullable = false, length = 128)
private String name;
@Column(length = 512)
private String description;
@Column(name = "is_system", nullable = false)
private boolean system;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@PrePersist
void prePersist() { this.createdAt = LocalDateTime.now(); }
public Long getId() { return id; }
public String getCode() { return code; }
public String getName() { return name; }
public boolean isSystem() { return system; }
}

View file

@ -0,0 +1,40 @@
package com.iflytek.skillhub.auth.entity;
import jakarta.persistence.*;
import java.io.Serializable;
import java.util.Objects;
@Entity
@Table(name = "role_permission")
@IdClass(RolePermission.RolePermissionId.class)
public class RolePermission {
@Id
@Column(name = "role_id")
private Long roleId;
@Id
@Column(name = "permission_id")
private Long permissionId;
public Long getRoleId() { return roleId; }
public Long getPermissionId() { return permissionId; }
public static class RolePermissionId implements Serializable {
private Long roleId;
private Long permissionId;
public RolePermissionId() {}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof RolePermissionId that)) return false;
return Objects.equals(roleId, that.roleId) && Objects.equals(permissionId, that.permissionId);
}
@Override
public int hashCode() {
return Objects.hash(roleId, permissionId);
}
}
}

View file

@ -0,0 +1,37 @@
package com.iflytek.skillhub.auth.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_role_binding",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role_id"}))
public class UserRoleBinding {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "role_id", nullable = false)
private Role role;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
protected UserRoleBinding() {}
public UserRoleBinding(Long userId, Role role) {
this.userId = userId;
this.role = role;
}
@PrePersist
void prePersist() { this.createdAt = LocalDateTime.now(); }
public Long getId() { return id; }
public Long getUserId() { return userId; }
public Role getRole() { return role; }
}

View file

@ -0,0 +1,13 @@
package com.iflytek.skillhub.auth.repository;
import com.iflytek.skillhub.auth.entity.ApiToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ApiTokenRepository extends JpaRepository<ApiToken, Long> {
Optional<ApiToken> findByTokenHash(String tokenHash);
List<ApiToken> findByUserIdAndRevokedAtIsNullOrderByCreatedAtDesc(Long userId);
}

View file

@ -0,0 +1,11 @@
package com.iflytek.skillhub.auth.repository;
import com.iflytek.skillhub.auth.entity.IdentityBinding;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface IdentityBindingRepository extends JpaRepository<IdentityBinding, Long> {
Optional<IdentityBinding> findByProviderCodeAndSubject(String providerCode, String subject);
}

View file

@ -0,0 +1,11 @@
package com.iflytek.skillhub.auth.repository;
import com.iflytek.skillhub.auth.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByCode(String code);
}

View file

@ -0,0 +1,11 @@
package com.iflytek.skillhub.auth.repository;
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRoleBindingRepository extends JpaRepository<UserRoleBinding, Long> {
List<UserRoleBinding> findByUserId(Long userId);
}

View file

@ -0,0 +1,61 @@
package com.iflytek.skillhub.domain.namespace;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "namespace")
public class Namespace {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 64)
private String slug;
@Column(name = "display_name", nullable = false, length = 128)
private String displayName;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private NamespaceStatus status = NamespaceStatus.ACTIVE;
@Column(length = 512)
private String description;
@Column(name = "created_by")
private Long createdBy;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
protected Namespace() {}
public Namespace(String slug, String displayName, Long createdBy) {
this.slug = slug;
this.displayName = displayName;
this.createdBy = createdBy;
}
@PrePersist
void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
}
@PreUpdate
void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public String getSlug() { return slug; }
public String getDisplayName() { return displayName; }
public NamespaceStatus getStatus() { return status; }
public Long getCreatedBy() { return createdBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}

View file

@ -0,0 +1,46 @@
package com.iflytek.skillhub.domain.namespace;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "namespace_member",
uniqueConstraints = @UniqueConstraint(columnNames = {"namespace_id", "user_id"}))
public class NamespaceMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "namespace_id", nullable = false)
private Long namespaceId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private NamespaceRole role;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
protected NamespaceMember() {}
public NamespaceMember(Long namespaceId, Long userId, NamespaceRole role) {
this.namespaceId = namespaceId;
this.userId = userId;
this.role = role;
}
@PrePersist
void prePersist() {
this.createdAt = LocalDateTime.now();
}
public Long getId() { return id; }
public Long getNamespaceId() { return namespaceId; }
public Long getUserId() { return userId; }
public NamespaceRole getRole() { return role; }
public void setRole(NamespaceRole role) { this.role = role; }
public LocalDateTime getCreatedAt() { return createdAt; }
}

View file

@ -0,0 +1,10 @@
package com.iflytek.skillhub.domain.namespace;
import java.util.List;
import java.util.Optional;
public interface NamespaceMemberRepository {
Optional<NamespaceMember> findByNamespaceIdAndUserId(Long namespaceId, Long userId);
List<NamespaceMember> findByUserId(Long userId);
NamespaceMember save(NamespaceMember member);
}

View file

@ -0,0 +1,9 @@
package com.iflytek.skillhub.domain.namespace;
import java.util.Optional;
public interface NamespaceRepository {
Optional<Namespace> findById(Long id);
Optional<Namespace> findBySlug(String slug);
Namespace save(Namespace namespace);
}

View file

@ -0,0 +1,5 @@
package com.iflytek.skillhub.domain.namespace;
public enum NamespaceRole {
OWNER, ADMIN, MEMBER
}

View file

@ -0,0 +1,5 @@
package com.iflytek.skillhub.domain.namespace;
public enum NamespaceStatus {
ACTIVE, FROZEN, ARCHIVED
}

View file

@ -0,0 +1,69 @@
package com.iflytek.skillhub.domain.user;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_account")
public class UserAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "display_name", nullable = false, length = 128)
private String displayName;
@Column(length = 256)
private String email;
@Column(name = "avatar_url", length = 512)
private String avatarUrl;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "merged_to_user_id")
private Long mergedToUserId;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
protected UserAccount() {}
public UserAccount(String displayName, String email, String avatarUrl) {
this.displayName = displayName;
this.email = email;
this.avatarUrl = avatarUrl;
this.status = UserStatus.ACTIVE;
}
@PrePersist
void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
}
@PreUpdate
void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getAvatarUrl() { return avatarUrl; }
public void setAvatarUrl(String avatarUrl) { this.avatarUrl = avatarUrl; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public Long getMergedToUserId() { return mergedToUserId; }
public void setMergedToUserId(Long mergedToUserId) { this.mergedToUserId = mergedToUserId; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public boolean isActive() { return this.status == UserStatus.ACTIVE; }
}

View file

@ -0,0 +1,8 @@
package com.iflytek.skillhub.domain.user;
import java.util.Optional;
public interface UserAccountRepository {
Optional<UserAccount> findById(Long id);
UserAccount save(UserAccount user);
}

View file

@ -0,0 +1,5 @@
package com.iflytek.skillhub.domain.user;
public enum UserStatus {
ACTIVE, PENDING, DISABLED, MERGED
}

View file

@ -15,5 +15,9 @@
<groupId>com.iflytek.skillhub</groupId>
<artifactId>skillhub-domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,14 @@
package com.iflytek.skillhub.infra.jpa;
import com.iflytek.skillhub.domain.namespace.Namespace;
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface NamespaceJpaRepository
extends JpaRepository<Namespace, Long>, NamespaceRepository {
Optional<Namespace> findBySlug(String slug);
}

View file

@ -0,0 +1,16 @@
package com.iflytek.skillhub.infra.jpa;
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface NamespaceMemberJpaRepository
extends JpaRepository<NamespaceMember, Long>, NamespaceMemberRepository {
Optional<NamespaceMember> findByNamespaceIdAndUserId(Long namespaceId, Long userId);
List<NamespaceMember> findByUserId(Long userId);
}

View file

@ -0,0 +1,11 @@
package com.iflytek.skillhub.infra.jpa;
import com.iflytek.skillhub.domain.user.UserAccount;
import com.iflytek.skillhub.domain.user.UserAccountRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserAccountJpaRepository
extends JpaRepository<UserAccount, Long>, UserAccountRepository {
}