package wf.spring;
import cn.apiclub.captcha.Captcha;
import cn.apiclub.captcha.backgrounds.FlatColorBackgroundProducer;
import cn.apiclub.captcha.backgrounds.GradiatedBackgroundProducer;
import cn.apiclub.captcha.gimpy.DropShadowGimpyRenderer;
import cn.apiclub.captcha.gimpy.RippleGimpyRenderer;
import cn.apiclub.captcha.text.renderer.ColoredEdgesWordRenderer;
import cn.apiclub.captcha.text.renderer.DefaultWordRenderer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Objects;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.imageio.ImageIO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;
import reactor.util.Logger;
import wf.data.MemberManager;
import wf.model.Member;
import wf.model.WebSocketRedisListener;
import wf.util.Loggers4j2;
/**
*
* @author Kent Yeh
*/
@Controller
public class RootController {
private static final Logger logger = Loggers4j2.getLogger(RootController.class);
public static final Random RAND = new SecureRandom();
private ObjectMapper objectMapper;
private MemberManager memberManager;
@Autowired
public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Autowired
public void setManager(MemberManager memberManager) {
this.memberManager = memberManager;
}
//
private static final List COLORS = new ArrayList<>(2);
private static final List FONTS = new ArrayList<>(3);
private static final char[] DEFAULT_CHARS = {'A', 'B', 'C', 'D', 'e', 'E', 'F',
'g', 'H', 'K', 'k', 'L', 'M', 'm', 'N', 'n', '2', '3', '4', '5', '6', '7', '8', '9',
'N', 'P', 'p', 'R', 'S', 'T', 'U', 'V', 'W', 'w', 'X', 'x', 'Y', 'y', 'Z', 'z'};
static {
COLORS.add(Color.BLACK);
COLORS.add(Color.BLUE);
COLORS.add(Color.CYAN);
COLORS.add(Color.GREEN);
COLORS.add(Color.MAGENTA);
COLORS.add(Color.ORANGE);
COLORS.add(Color.PINK);
COLORS.add(Color.RED);
COLORS.add(Color.YELLOW);
FONTS.add(new Font("Times New Roman", Font.BOLD, 24));
FONTS.add(new Font("Times New Roman", Font.ITALIC, 24));
FONTS.add(new Font("Courier", Font.BOLD, 24));
FONTS.add(new Font("Courier", Font.ITALIC, 24));
FONTS.add(new Font("Monospace", Font.BOLD, 24));
FONTS.add(new Font("Monospace", Font.ITALIC, 24));
FONTS.add(new Font("Arial", Font.BOLD, 32));
FONTS.add(new Font("Arial", Font.ITALIC, 28));
}
private String getChptcha(int len) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) {
sb.append(DEFAULT_CHARS[RAND.nextInt(DEFAULT_CHARS.length)]);
}
return sb.toString();
}
private Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + RAND.nextInt(bc - fc);
int g = fc + RAND.nextInt(bc - fc);
int b = fc + RAND.nextInt(bc - fc);
return new Color(r, g, b);
}
//
@Value("#{systemProperties['captcha'] }")
private String defaultCaptcha;
@GetMapping({"/", "/index"})
public String index() {
return "index";
}
@GetMapping("/whoami")
public String whomai(@AuthenticationPrincipal Mono principal, Model model) {
model.addAttribute("user", principal);
return "index";
}
/**
*
* @return PreAuthorize 回傳值必須是Mono/Flux 不可
*/
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public Mono testAdmin() {
return Mono.just("admin");
}
@GetMapping("/static/captcha")
public Mono> captcha(WebSession session,
@RequestParam(name = "key", defaultValue = "captcha") String cpkey,
@RequestParam(name = "len", defaultValue = "4") final int len) throws Exception {
boolean rgr = RAND.nextBoolean();
Captcha captcha = new Captcha.Builder(80, 36)
//
.addNoise((BufferedImage bi) -> {
Graphics2D g = bi.createGraphics();
int h = bi.getHeight();
int w = bi.getWidth();
for (int i = 0; i < 155; i++) {
int x = RAND.nextInt(w);
int y = RAND.nextInt(h);
int xl = RAND.nextInt(10);
int yl = RAND.nextInt(4);
g.setColor(rgr ? getRandColor(32, 48) : getRandColor(0, 196));
g.drawLine(x, y, x + xl, y + yl);
}
})
.addText(() -> {
//
String val = defaultCaptcha == null || defaultCaptcha.trim().isEmpty() ? getChptcha(len) : defaultCaptcha.trim();
session.getAttributes().put(cpkey, val);
return val;
//
}, RAND.nextBoolean() ? new ColoredEdgesWordRenderer(COLORS, FONTS) : new DefaultWordRenderer(COLORS, FONTS))
.gimp(rgr ? new RippleGimpyRenderer() : new DropShadowGimpyRenderer())
.addBorder()
.addBackground(RAND.nextBoolean() ? new FlatColorBackgroundProducer() : new GradiatedBackgroundProducer())
//
.build();
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(captcha.getImage(), "png", baos);
byte[] data = baos.toByteArray();
MultiValueMap properties = new LinkedMultiValueMap<>();
properties.add("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
properties.add("Content-Type", MediaType.IMAGE_PNG_VALUE);//如果用produces在舊版FF會有產生406錯誤
return Mono.just(new ResponseEntity<>(data, new HttpHeaders(properties), HttpStatus.OK));
}
}
@GetMapping("/login")
public Mono getLoginPage() {
return Mono.just("login");
}
@GetMapping("/hello/{member}")
public String hello(@PathVariable("member") Member.Mono member, Model model) {
model.addAttribute("user", member.get().switchIfEmpty(Mono.empty()));
return "hello";
}
@PostMapping("/putmyshtoppingcart")
public Mono> putAstaff(WebSession session,
@AuthenticationPrincipal User principal) {
List wsrls = FluxWebSocketHandler.userListenser.get(session.getId());
if (wsrls == null || wsrls.isEmpty()) {
logger.error("{}沒有登錄資料", principal.getUsername());
return Mono.just(new ResponseEntity<>("沒有登錄資料", HttpStatus.FORBIDDEN));
} else {
ObjectNode json = objectMapper.createObjectNode();
json.put("shoppingcard", "您剛把東西放進購物車");
JsonNode jn = objectMapper.convertValue(json, JsonNode.class);
logger.info("publish {} {}", principal.getUsername(), jn.toString());
return wsrls.get(0).getReactiveRedisTemplate().convertAndSend(principal.getUsername(), jn)
.then(Mono.just(new ResponseEntity<>("已放入購物車", HttpStatus.OK)));
}
}
@GetMapping("/changePassword")
public String getChangePassword() {
return "changePassword";
}
@PostMapping("/changePassword")
public Mono changePassword(@AuthenticationPrincipal UsernamePasswordAuthenticationToken principal,
ServerWebExchange exchange, Model model) {
return exchange.getFormData().doOnNext(mvm -> {
if (Objects.equal(mvm.getFirst("oldPass"), mvm.getFirst("newPass"))) {
model.addAttribute("message", "新舊密碼不可以相同");
} else if (!Objects.equal(mvm.getFirst("newPass"), mvm.getFirst("confirmPass"))) {
model.addAttribute("message", "確認密碼與新密碼不符");
}
})
.filter(mvm -> model.getAttribute("message") == null)
.flatMap(mvm -> {
String newPass = mvm.getFirst("newPass");
String oldPass = mvm.getFirst("oldPass");
return memberManager.changePassword(principal.getName(), newPass, oldPass)
.doOnNext(i -> {
model.addAttribute("message", i > 0 ? "密碼更新成功" : "更新密碼失敗");
})
.map(i -> i > 0 ? "index" : "changePassword");
}).switchIfEmpty(Mono.just("changePassword"));
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/modifyMember/{member}")
public Mono memberAdminView(@PathVariable("member") Member.Mono member, Model model) {
model.addAttribute("member", member.get());
return Mono.just("member");
}
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/modifyMember/{member}")
public Mono modifyMember(@PathVariable("member") Member.Mono oriMember,
@ModelAttribute Member member, Model model) {
//避免後面有值但第一個沒勾,導致null字串
Iterables.removeIf(member.getRoles(), Predicates.isNull());
model.addAttribute("member", memberManager.saveMember(member.setNew(false)));
return Mono.just("member");
}
@GetMapping("/ws")
public String chat() {
return "chat";
}
}