Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ summary*.html

application.properties
moadong.json
firebase.json
firebase.json

/.cursor
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package moadong.club.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import moadong.club.payload.request.ClubInfoRequest;
import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
import moadong.club.payload.response.ClubListResponse;
import moadong.club.service.ClubProfileService;
import moadong.global.payload.Response;
import moadong.user.annotation.CurrentUser;
import moadong.user.payload.CustomUserDetails;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/admin")
@AllArgsConstructor
@Tag(name = "Club Admin", description = "동아리 관리자 API (개발자 전용)")
public class ClubAdminController {

private final ClubProfileService clubProfileService;

@GetMapping("/clubs")
@Operation(summary = "동아리 목록 조회", description = "전체 동아리 목록을 조회합니다. DEVELOPER 역할 필요.")
@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?> getAllClubs() {
ClubListResponse response = clubProfileService.getAllClubsForAdmin();
return Response.ok(response);
}

@PutMapping("/club/{clubId}/info")
@Operation(summary = "동아리 약력 수정 (관리자)", description = "지정한 clubId 동아리의 약력을 수정합니다. DEVELOPER 역할 필요.")
@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?> updateClubInfo(
@CurrentUser CustomUserDetails user,
@PathVariable String clubId,
@RequestBody @Valid ClubInfoRequest request) {
clubProfileService.updateClubInfoByClubId(clubId, request, user);
return Response.ok("success update club info");
}

@PutMapping("/club/{clubId}/description")
@Operation(summary = "동아리 모집정보 수정 (관리자)", description = "지정한 clubId 동아리의 모집정보를 수정합니다. DEVELOPER 역할 필요.")
@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?> updateClubDescription(
@CurrentUser CustomUserDetails user,
@PathVariable String clubId,
@RequestBody ClubRecruitmentInfoUpdateRequest request) {
clubProfileService.updateClubRecruitmentInfoByClubId(clubId, request, user);
return Response.ok("success update club description");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package moadong.club.payload.response;

import java.util.List;

public record ClubListResponse(List<ClubListResponseItem> clubs) {

public record ClubListResponseItem(String id, String name, String userId) {
}
}
40 changes: 40 additions & 0 deletions backend/src/main/java/moadong/club/service/ClubProfileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import moadong.media.resolver.ImageDisplayUrlResolver;
import moadong.club.payload.request.ClubRecruitmentInfoUpdateRequest;
import moadong.club.payload.response.ClubDetailedResponse;
import moadong.club.payload.response.ClubListResponse;
import moadong.club.repository.ClubRepository;
import moadong.club.repository.ClubSearchRepository;
import moadong.club.util.RecruitmentStateCalculator;
Expand Down Expand Up @@ -60,6 +61,18 @@ public void updateClubRecruitmentInfo(ClubRecruitmentInfoUpdateRequest request,
javers.commit(user.getUsername(), saved);
}

public ClubListResponse getAllClubsForAdmin() {
List<Club> all = clubRepository.findAll();
List<ClubListResponse.ClubListResponseItem> items = all.stream()
.map(c -> new ClubListResponse.ClubListResponseItem(
c.getId() != null ? c.getId() : "",
c.getName() != null ? c.getName() : "",
c.getUserId() != null ? c.getUserId() : ""
))
.toList();
return new ClubListResponse(items);
}

public ClubDetailedResponse getClubDetail(String clubId) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
Expand All @@ -73,5 +86,32 @@ public ClubDetailedResponse getClubDetail(String clubId) {
clubDetailedResult.feeds());
return new ClubDetailedResponse(clubDetailedResult);
}

@Transactional
public void updateClubInfoByClubId(String clubId, ClubInfoRequest request, CustomUserDetails user) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
club.update(request);
Club saved = clubRepository.save(club);
javers.commit(user.getUsername(), saved);
}

@Transactional
public void updateClubRecruitmentInfoByClubId(String clubId, ClubRecruitmentInfoUpdateRequest request,
CustomUserDetails user) {
ObjectId objectId = ObjectIdConverter.convertString(clubId);
Club club = clubRepository.findClubById(objectId)
.orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
club.update(request);
recruitmentStateCalculator.calculate(
club,
club.getClubRecruitmentInformation().getRecruitmentStart(),
club.getClubRecruitmentInformation().getRecruitmentEnd()
);
club.getClubRecruitmentInformation().updateLastModifiedDate();
Club saved = clubRepository.save(club);
javers.commit(user.getUsername(), saved);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ public SecurityConfig(JwtProvider jwtProvider, CustomUserDetailService userDetai
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll() // 모든 요청에 대해 인증 해제
.requestMatchers("/api/admin/**").hasRole("DEVELOPER")
.anyRequest().permitAll()
);

http.addFilterBefore(new JwtAuthenticationFilter(jwtProvider, userDetailsService), UsernamePasswordAuthenticationFilter.class);


return http.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package moadong.global.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DevPortalController {

@GetMapping({"/dev", "/dev/"})
public String devPortal() {
return "redirect:/dev/index.html";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package moadong.user.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import moadong.global.payload.Response;
import moadong.user.payload.request.DevRegisterRequest;
import moadong.user.service.UserCommandService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth/dev")
@AllArgsConstructor
@Tag(name = "Dev Auth", description = "개발자 전용 계정 등록 API (시크릿 키 필요)")
public class DevAuthController {

private final UserCommandService userCommandService;

@PostMapping("/register")
@Operation(summary = "개발자 계정 등록", description = "app.dev-registration-secret과 일치하는 secret을 보내야 합니다. 개발자만 /dev, /api/admin/** 접근 가능.")
public ResponseEntity<?> registerDeveloper(@RequestBody @Valid DevRegisterRequest request) {
userCommandService.registerDeveloper(request);
return Response.ok("success register developer");
}
}
8 changes: 7 additions & 1 deletion backend/src/main/java/moadong/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import moadong.global.annotation.UserId;
import moadong.user.entity.enums.UserRole;
import moadong.user.entity.enums.UserStatus;
import moadong.user.payload.request.UserUpdateRequest;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Builder
Expand Down Expand Up @@ -49,6 +51,9 @@ public class User implements UserDetails {
@NotNull
private String clubId;

@Builder.Default
private UserRole role = UserRole.CLUB_ADMIN;

@Builder.Default
@NotNull
private Date createdAt = new Date();
Expand All @@ -68,7 +73,8 @@ public class User implements UserDetails {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
UserRole effectiveRole = (role != null) ? role : UserRole.CLUB_ADMIN;
return List.of(new SimpleGrantedAuthority("ROLE_" + effectiveRole.name()));
}

@Override
Expand Down
6 changes: 6 additions & 0 deletions backend/src/main/java/moadong/user/entity/enums/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package moadong.user.entity.enums;

public enum UserRole {
CLUB_ADMIN,
DEVELOPER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package moadong.user.payload.request;

import jakarta.validation.constraints.NotBlank;
import moadong.global.annotation.Korean;
import moadong.global.annotation.Password;
import moadong.global.annotation.PhoneNumber;
import moadong.global.annotation.UserId;

public record DevRegisterRequest(
@NotBlank
@UserId
String userId,
@NotBlank
@Password
String password,
@NotBlank
@Korean
String name,
@PhoneNumber
String phoneNumber,
@NotBlank
String secret
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import moadong.global.exception.RestApiException;
import moadong.user.entity.User;
import moadong.user.entity.UserInformation;
import moadong.user.entity.enums.UserRole;
import org.springframework.security.crypto.password.PasswordEncoder;

public record UserRegisterRequest(
Expand All @@ -31,10 +32,15 @@ public record UserRegisterRequest(
}

public User toUserEntity(PasswordEncoder passwordEncoder) {
return toUserEntity(passwordEncoder, UserRole.CLUB_ADMIN);
}

public User toUserEntity(PasswordEncoder passwordEncoder, UserRole role) {
return User.builder()
.userId(userId)
.password(passwordEncoder.encode(password))
.userInformation(new UserInformation(name,phoneNumber))
.userInformation(new UserInformation(name, phoneNumber))
.role(role != null ? role : UserRole.CLUB_ADMIN)
.build();
}

Expand Down
39 changes: 36 additions & 3 deletions backend/src/main/java/moadong/user/service/UserCommandService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.Date;

import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import moadong.club.entity.Club;
import moadong.club.repository.ClubRepository;
import moadong.global.exception.ErrorCode;
Expand All @@ -15,6 +15,8 @@
import moadong.user.entity.RefreshToken;
import moadong.user.entity.User;
import moadong.user.payload.CustomUserDetails;
import moadong.user.entity.enums.UserRole;
import moadong.user.payload.request.DevRegisterRequest;
import moadong.user.payload.request.UserLoginRequest;
import moadong.user.payload.request.UserRegisterRequest;
import moadong.user.payload.request.UserUpdateRequest;
Expand All @@ -28,11 +30,12 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class UserCommandService {

private final UserRepository userRepository;
Expand All @@ -43,6 +46,9 @@ public class UserCommandService {
private final CookieMaker cookieMaker;
private final SecurePasswordGenerator securePasswordGenerator;

@Value("${app.dev-registration-secret:}")
private String devRegistrationSecret; // set by Spring after construction (field injection)

@Transactional
public User registerUser(UserRegisterRequest userRegisterRequest) {
String userId = new ObjectId().toHexString();
Expand All @@ -58,6 +64,29 @@ public User registerUser(UserRegisterRequest userRegisterRequest) {
}
}

@Transactional
public User registerDeveloper(DevRegisterRequest request) {
if (devRegistrationSecret == null || devRegistrationSecret.isBlank()
|| !devRegistrationSecret.equals(request.secret())) {
throw new RestApiException(ErrorCode.USER_UNAUTHORIZED);
}
UserRegisterRequest baseRequest = new UserRegisterRequest(
request.userId(),
request.password(),
request.name(),
request.phoneNumber()
);
String userId = new ObjectId().toHexString();
String clubId = new ObjectId().toHexString();
try {
User user = userRepository.save(createUserWithRole(baseRequest, userId, clubId, UserRole.DEVELOPER));
createClub(clubId, userId);
return user;
} catch (MongoWriteException e) {
throw new RestApiException(ErrorCode.USER_ALREADY_EXIST);
}
}

public LoginResponse loginUser(UserLoginRequest userLoginRequest,
HttpServletResponse response) {
try {
Expand Down Expand Up @@ -179,7 +208,11 @@ public String findClubIdByUserId(String userID) {
}

private User createUser(UserRegisterRequest request, String userId, String clubId) {
User user = request.toUserEntity(passwordEncoder);
return createUserWithRole(request, userId, clubId, UserRole.CLUB_ADMIN);
}

private User createUserWithRole(UserRegisterRequest request, String userId, String clubId, UserRole role) {
User user = request.toUserEntity(passwordEncoder, role);
user.updateId(userId);
user.updateClubId(clubId);
return user;
Expand Down
Loading
Loading