1
0
mirror of https://github.com/FatttSnake/Pinnacle-OA.git synced 2026-04-04 22:41:24 +08:00

Added login, logout and getUserinfo (Include ui and server)

This commit is contained in:
2023-05-05 20:59:09 +08:00
parent a8dce8f8e0
commit 60b8460e03
32 changed files with 1022 additions and 151 deletions

View File

@@ -14,6 +14,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
public class SecurityConfig {
@@ -42,7 +47,7 @@ public class SecurityConfig {
}
@Bean
AuthenticationManager authenticationManager(HttpSecurity httpSecurity, PasswordEncoder passwordEncoder) throws Exception {
public AuthenticationManager authenticationManager(HttpSecurity httpSecurity, PasswordEncoder passwordEncoder) throws Exception {
return httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder)
@@ -50,22 +55,38 @@ public class SecurityConfig {
.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedMethods(List.of("*"));
corsConfiguration.setAllowedHeaders(List.of("*"));
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowedOrigins(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",corsConfiguration);
return source;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
// Disable CSRF
.csrf().disable()
.csrf()
.disable()
// Do not get SecurityContent by Session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Allow anonymous access
.authorizeHttpRequests()
.requestMatchers("/login").anonymous()
.requestMatchers("/login")
.anonymous()
// Authentication required
.anyRequest().authenticated()
.anyRequest()
.authenticated()
.and()
.logout()
@@ -75,6 +96,10 @@ public class SecurityConfig {
.authenticationEntryPoint(authenticationEntryPointHandler)
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

View File

@@ -1,8 +1,17 @@
package com.cfive.pinnacle.controller.permission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.Element;
import com.cfive.pinnacle.service.permission.IElementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 页面元素 前端控制器
@@ -14,5 +23,26 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/element")
public class ElementController {
private IElementService elementService;
@Autowired
public void setElementService(IElementService elementService) {
this.elementService = elementService;
}
@GetMapping
public ResponseResult getAllElement() {
List<Element> elements = elementService.list();
return ResponseResult.databaseSelectSuccess(elements);
}
@GetMapping("/{id}")
public ResponseResult getElement(@PathVariable long id) {
LambdaQueryWrapper<Element> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Element::getId, id);
Element element = elementService.getOne(wrapper);
return ResponseResult.databaseSelectSuccess(element);
}
}

View File

@@ -1,8 +1,17 @@
package com.cfive.pinnacle.controller.permission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.File;
import com.cfive.pinnacle.service.permission.IFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 文件 前端控制器
@@ -14,5 +23,26 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/file")
public class FileController {
private IFileService fileService;
@Autowired
public void setFileService(IFileService fileService) {
this.fileService = fileService;
}
@GetMapping
public ResponseResult getAllFile() {
List<File> files = fileService.list();
return ResponseResult.databaseSelectSuccess(files);
}
@GetMapping("/{id}")
public ResponseResult getFile(@PathVariable int id) {
LambdaQueryWrapper<File> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(File::getId, id);
File file = fileService.getOne(wrapper);
return ResponseResult.databaseSelectSuccess(file);
}
}

View File

@@ -5,14 +5,14 @@ import com.cfive.pinnacle.entity.common.ResponseCode;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.service.permission.ILoginService;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@RestController
@CrossOrigin
public class LoginController {
private ILoginService loginService;
@@ -37,4 +37,11 @@ public class LoginController {
return ResponseResult.build(ResponseCode.LOGOUT_FAILED, "Logout Failed", null);
}
}
@GetMapping("/userInfo")
public ResponseResult getUserInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
return ResponseResult.success(principal);
}
}

View File

@@ -1,8 +1,17 @@
package com.cfive.pinnacle.controller.permission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.Menu;
import com.cfive.pinnacle.service.permission.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 菜单 前端控制器
@@ -14,5 +23,26 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/menu")
public class MenuController {
private IMenuService menuService;
@Autowired
public void setMenuService(IMenuService menuService) {
this.menuService = menuService;
}
@GetMapping
public ResponseResult getAllMenu() {
List<Menu> menus = menuService.list();
return ResponseResult.databaseSelectSuccess(menus);
}
@GetMapping("/{id}")
public ResponseResult getMenu(@PathVariable int id) {
LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Menu::getId, id);
Menu menu = menuService.getOne(wrapper);
return ResponseResult.databaseSelectSuccess(menu);
}
}

View File

@@ -1,8 +1,17 @@
package com.cfive.pinnacle.controller.permission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.Operation;
import com.cfive.pinnacle.service.permission.IOperationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 功能 前端控制器
@@ -14,5 +23,26 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/operation")
public class OperationController {
private IOperationService operationService;
@Autowired
public void setOperationService(IOperationService operationService) {
this.operationService = operationService;
}
@GetMapping
public ResponseResult getAllOperation() {
List<Operation> operations = operationService.list();
return ResponseResult.databaseSelectSuccess(operations);
}
@GetMapping("/{id}")
public ResponseResult getOperation(@PathVariable int id) {
LambdaQueryWrapper<Operation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Operation::getId, id);
Operation operation = operationService.getOne(wrapper);
return ResponseResult.databaseSelectSuccess(operation);
}
}

View File

@@ -1,8 +1,17 @@
package com.cfive.pinnacle.controller.permission;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.OperationLog;
import com.cfive.pinnacle.service.permission.IOperationLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 操作日志 前端控制器
@@ -14,5 +23,26 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/operationLog")
public class OperationLogController {
private IOperationLogService operationLogService;
@Autowired
public void setOperationLogService(IOperationLogService operationLogService) {
this.operationLogService = operationLogService;
}
@GetMapping
public ResponseResult getAllOperationLog() {
List<OperationLog> operationLogs = operationLogService.list();
return ResponseResult.databaseSelectSuccess(operationLogs);
}
@GetMapping("/{id}")
public ResponseResult getOperationLog(@PathVariable int id) {
LambdaQueryWrapper<OperationLog> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OperationLog::getId, id);
OperationLog operationLog = operationLogService.getOne(wrapper);
return ResponseResult.databaseSelectSuccess(operationLog);
}
}

View File

@@ -1,5 +1,10 @@
package com.cfive.pinnacle.controller.permission;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.PowerSet;
import com.cfive.pinnacle.service.permission.IPowerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -14,5 +19,17 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/power")
public class PowerController {
private IPowerService powerService;
@Autowired
public void setPowerService(IPowerService powerService) {
this.powerService = powerService;
}
@GetMapping
public ResponseResult getAllPower() {
PowerSet powerSet = powerService.getAllPower();
return ResponseResult.databaseSelectSuccess(powerSet);
}
}

View File

@@ -1,11 +1,11 @@
package com.cfive.pinnacle.controller.permission;
import com.cfive.pinnacle.entity.common.ResponseCode;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cfive.pinnacle.entity.common.ResponseResult;
import com.cfive.pinnacle.entity.permission.*;
import com.cfive.pinnacle.service.permission.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -22,16 +22,25 @@ import java.util.List;
@RestController
@RequestMapping("/powerType")
public class PowerTypeController {
IPowerService powerTypeService;
IPowerTypeService powerTypeService;
@Autowired
public void setPowerTypeService(IPowerService powerTypeService) {
public void setPowerTypeService(IPowerTypeService powerTypeService) {
this.powerTypeService = powerTypeService;
}
@GetMapping
public ResponseResult getAllPowerType() {
List<Power> powerTypes = powerTypeService.list();
return ResponseResult.build(ResponseCode.DATABASE_SELECT_OK, "success", powerTypes);
List<PowerType> powerTypes = powerTypeService.list();
return ResponseResult.databaseSelectSuccess(powerTypes);
}
@GetMapping("/{id}")
public ResponseResult getPowerType(@PathVariable int id) {
LambdaQueryWrapper<PowerType> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PowerType::getId, id);
PowerType powerType = powerTypeService.getOne(wrapper);
return ResponseResult.databaseSelectSuccess(powerType);
}
}

View File

@@ -55,4 +55,20 @@ public class ResponseResult implements Serializable {
public static ResponseResult fail(String msg, Object data) {
return build(ResponseCode.SYSTEM_ERROR, msg, data);
}
public static ResponseResult databaseSelectSuccess(Object object) {
return build(ResponseCode.DATABASE_SELECT_OK, "success", object);
}
public static ResponseResult databaseSaveSuccess(Object object) {
return build(ResponseCode.DATABASE_SAVE_OK, "success", object);
}
public static ResponseResult databaseUpdateSuccess(Object object) {
return build(ResponseCode.DATABASE_UPDATE_OK, "success", object);
}
public static ResponseResult databaseDeleteSuccess() {
return build(ResponseCode.DATABASE_DELETE_OK, "success", null);
}
}

View File

@@ -1,6 +1,7 @@
package com.cfive.pinnacle.entity.permission;
import com.cfive.pinnacle.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -14,102 +15,46 @@ import java.util.Collection;
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private Collection<? extends GrantedAuthority> authorities;
private String password;
private String username;
private Boolean accountNonExpired = true;
private Boolean accountNonLocked = true;
private Boolean credentialsNonExpired = true;
private Boolean enabled = true;
public LoginUser(User user) {
this.user = user;
this.username = user.getUsername();
this.password = user.getPasswd();
this.enabled = user.getEnable() == 1;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
return null;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
return user.getPasswd();
}
@JsonIgnore
@Override
public String getUsername() {
return username;
return user.getUsername();
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return this.enabled;
}
public void setUsername(String username) {
this.username = username;
}
public Boolean getAccountNonExpired() {
return accountNonExpired;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public Boolean getAccountNonLocked() {
return accountNonLocked;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public Boolean getCredentialsNonExpired() {
return credentialsNonExpired;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
return user.getEnable() == 1;
}
}

View File

@@ -45,12 +45,6 @@ public class Operation implements Serializable {
@TableField("code")
private String code;
/**
* URL 前缀
*/
@TableField("url_prefix")
private String urlPrefix;
/**
* 权限ID
*/

View File

@@ -0,0 +1,28 @@
package com.cfive.pinnacle.entity.permission;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PowerSet implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private List<Operation> operationList;
private List<Menu> menuList;
private List<Element> elementList;
private List<File> fileList;
}

View File

@@ -49,7 +49,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
if (Objects.isNull(loginUser)) {
throw new RuntimeException("Not logged in");
}
// Todo 权限
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

View File

@@ -2,6 +2,7 @@ package com.cfive.pinnacle.service.permission;
import com.cfive.pinnacle.entity.permission.Power;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cfive.pinnacle.entity.permission.PowerSet;
/**
* <p>
@@ -12,5 +13,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
* @since 2023-04-30
*/
public interface IPowerService extends IService<Power> {
PowerSet getAllPower();
}

View File

@@ -39,6 +39,7 @@ public class LoginServiceImpl implements ILoginService {
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
loginUser.getUser().setPasswd("");
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);

View File

@@ -1,11 +1,14 @@
package com.cfive.pinnacle.service.permission.impl;
import com.cfive.pinnacle.entity.permission.Power;
import com.cfive.pinnacle.entity.permission.*;
import com.cfive.pinnacle.mapper.permission.PowerMapper;
import com.cfive.pinnacle.service.permission.IPowerService;
import com.cfive.pinnacle.service.permission.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 权限 服务实现类
@@ -16,5 +19,38 @@ import org.springframework.stereotype.Service;
*/
@Service
public class PowerServiceImpl extends ServiceImpl<PowerMapper, Power> implements IPowerService {
private IOperationService operationService;
private IMenuService menuService;
private IElementService elementService;
private IFileService fileService;
@Autowired
public void setOperationService(IOperationService operationService) {
this.operationService = operationService;
}
@Autowired
public void setMenuService(IMenuService menuService) {
this.menuService = menuService;
}
@Autowired
public void setElementService(IElementService elementService) {
this.elementService = elementService;
}
@Autowired
public void setFileService(IFileService fileService) {
this.fileService = fileService;
}
@Override
public PowerSet getAllPower() {
List<Operation> operationList = operationService.list();
List<Menu> menuList = menuService.list();
List<Element> elementList = elementService.list();
List<File> fileList = fileService.list();
return new PowerSet(operationList, menuList, elementList, fileList);
}
}

View File

@@ -29,6 +29,9 @@ public class UserDetailsServiceImpl implements UserDetailsService {
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("Username not found in database");
}
// Todo 权限
return new LoginUser(user);
}
}

View File

@@ -1,5 +1,6 @@
package com.cfive.pinnacle.service;
import com.cfive.pinnacle.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -18,4 +19,19 @@ class IUserServiceTest {
void getBCy(@Autowired PasswordEncoder passwordEncoder) {
System.out.println(passwordEncoder.encode("123"));
}
@Test
void addUser(@Autowired IUserService userService, @Autowired PasswordEncoder passwordEncoder) {
User user = new User();
user.setUsername("ggb");
user.setPasswd(passwordEncoder.encode("123"));
user.setDepartmentId(1652713919467151362L);
user.setEnable(1);
userService.save(user);
}
@Test
void removeUser(@Autowired IUserService userService) {
userService.removeById(1);
}
}

60
sql/Insert.sql Normal file
View File

@@ -0,0 +1,60 @@
SET FOREIGN_KEY_CHECKS=0;
truncate t_menu;
truncate t_element;
truncate t_file;
truncate t_operation_log;
truncate t_operation;
truncate t_power;
truncate t_power_type;
insert into t_power_type (id, name)
values (1, 'operation'),
(2, 'menu'),
(3, 'element'),
(4, 'file');
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, power_id, parent_id)
values ('Select All Power Type', 'system:power_type:all', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, power_id, parent_id)
values ('Select All Power', 'system:power:all', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, power_id, parent_id)
values ('Select All User', 'system:operation:all', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, power_id, parent_id)
values ('Select All User', 'system:menu:all', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, power_id, parent_id)
values ('Select All User', 'system:element:all', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, power_id, parent_id)
values ('Select All User', 'system:file:all', last_insert_id(), null);
commit;
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -69,7 +69,6 @@ create table `t_operation`
`id` bigint not null primary key auto_increment,
`name` varchar(50) not null comment '功能名',
`code` varchar(50) null comment '功能编码',
`url_prefix` varchar(100) null comment 'URL 前缀',
`power_id` bigint not null comment '权限ID',
`parent_id` bigint null comment '父ID',
constraint t_operation_power_id_fk foreign key (power_id) references t_power (id)
@@ -299,31 +298,3 @@ create table `t_attendance`
constraint t_attendance_user_id_fk foreign key (user_id) references t_user (id),
constraint t_attendance_modify_id_fk foreign key (modify_id) references t_user (id)
) comment '考勤';
insert into t_power_type (id, name)
values (1, 'operation'),
(2, 'menu'),
(3, 'element'),
(4, 'file');
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, url_prefix, power_id, parent_id)
values ('Select All Power Type', 'select_all_power_type', 'GET:/powerType', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, url_prefix, power_id, parent_id)
values ('Select All Power Type', 'select_all_power_type', 'GET:/powerType', last_insert_id(), null);
commit;
begin;
insert into t_power (type_id)
values (1);
insert into t_operation (name, code, url_prefix, power_id, parent_id)
values ('Select All User', 'select_all_user', 'GET:/user', last_insert_id(), null);
commit;

View File

@@ -22,7 +22,7 @@ module.exports = {
rules: {
"no-cond-assign": "error",
"eqeqeq": "error",
"indent": ["error", 4],
"indent": ["error", 4, {"SwitchCase": 1}],
"prettier/prettier": [
"error",
{

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="473.5501403808594" height="514.205810546875"
viewBox="0 0 473.5501403808594 514.205810546875"><g style="mix-blend-mode:passthrough"><g style="mix-blend-mode:passthrough" transform="matrix(-0.09544334560632706,-0.9953708648681641,0.9953532218933105,-0.09562418609857559,-475.4320658580816,596.4368332676222)"><path d="M207.21426391601562,514.205810546875L357.9022639160156,845.999810546875L56.52586391601562,845.999810546875L207.21426391601562,514.205810546875Z" fill="#5495F1" fill-opacity="1"/></g><g style="mix-blend-mode:passthrough" transform="matrix(0.6958200931549072,-0.7182161211967468,0.7182161211967468,0.6958200931549072,-42.65635399221446,124.80157307758964)"><path d="M204.50979614257812,112.7599334716797L272.49279614257813,317.36393347167973L136.52679614257812,317.36393347167973L204.50979614257812,112.7599334716797Z" fill="#FFFFFF" fill-opacity="0.6000000238418579"/></g></g></svg>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="27.428573608398438" height="32.000003814697266"
viewBox="0 0 27.428573608398438 32.000003814697266"><g><path d="M22.8571,10.6667L25.9048,10.6667C26.7463,10.6667,27.4286,11.3489,27.4286,12.1905L27.4286,30.4762C27.4286,31.3178,26.7463,32,25.9048,32L1.52381,32C0.682233,32,0,31.3178,0,30.4762L0,12.1905C0,11.3489,0.682233,10.6667,1.52381,10.6667L4.57143,10.6667L4.57143,9.14286C4.57143,4.0934,8.66483,0,13.7143,0C18.7637,0,22.8571,4.0934,22.8571,9.14286L22.8571,10.6667ZM19.8095,10.6667L19.8095,9.14286C19.8095,5.77655,17.0806,3.04762,13.7143,3.04762C10.348,3.04762,7.61905,5.77655,7.61905,9.14286L7.61905,10.6667L19.8095,10.6667ZM12.1905,19.8095L12.1905,22.8571L15.2381,22.8571L15.2381,19.8095L12.1905,19.8095ZM6.09524,19.8095L6.09524,22.8571L9.14286,22.8571L9.14286,19.8095L6.09524,19.8095ZM18.2857,19.8095L18.2857,22.8571L21.3333,22.8571L21.3333,19.8095L18.2857,19.8095Z" fill-opacity="1"/></g></svg>

After

Width:  |  Height:  |  Size: 966 B

View File

@@ -1,4 +1,5 @@
const PRODUCTION_NAME = 'Pinnacle OA'
const TOKEN_NAME = 'JWT_TOKEN'
const COLOR_PRODUCTION = '#00D4FF'
const COLOR_BACKGROUND = '#D8D8D8'
const COLOR_FONT_MAIN = '#4D4D4D'
@@ -11,6 +12,7 @@ const SIZE_ICON_XL = '64px'
export {
PRODUCTION_NAME,
TOKEN_NAME,
COLOR_PRODUCTION,
COLOR_BACKGROUND,
COLOR_FONT_MAIN,

View File

@@ -5,18 +5,6 @@ import router from '@/router'
import '@/assets/css/base.css'
import '@/assets/css/common.css'
/*
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
from.path ? next({ path: from.path }) : next('/')
} else {
if (to.meta.title) {
document.title = PRODUCTION_NAME + ' - ' + to.meta.title
}
}
})
*/
const app = createApp(App)
app.use(router).mount('#app')

273
ui/src/pages/Login.vue Normal file
View File

@@ -0,0 +1,273 @@
<template>
<div class="background" @keyup.enter="login">
<div class="card-back">
<div class="production-name">
<span class="emphasize">{{ PRODUCTION_NAME() }}</span>
<br />
<span>自动化办公系统</span>
</div>
<img class="back-shape" :src="backShape" alt="back-shape" />
</div>
<div class="card-front">
<div class="login-title">登录</div>
<div class="input-box user-name-box">
<div class="center-box" style="padding: 10px">
<el-icon size="18">
<icon-pinnacle-user />
</el-icon>
</div>
<label for="user-name"></label
><input
type="text"
name="user-name"
id="user-name"
v-model="userName"
@keyup="userName = userName.replace(/\s+/g, '')"
placeholder="用户名"
/>
</div>
<div class="input-box password-box">
<div class="center-box" style="padding: 10px">
<el-icon size="18">
<icon-pinnacle-password />
</el-icon>
</div>
<label for="password"></label
><input
type="password"
name="password"
id="password"
v-model="password"
@keyup="password = password.replace(/\s+/g, '')"
placeholder="密码"
/>
</div>
<div class="captcha-set">
<div class="captcha-box">
<div class="input-box" style="height: 100%">
<label for="captcha"></label
><input
type="text"
name="captcha"
id="captcha"
v-model="captcha"
placeholder="验证码"
/>
</div>
</div>
<img :src="captchaSrc" alt="Captcha" @click="getNewCaptcha" />
</div>
<ElButton
size="large"
type="primary"
:disabled="loggingIn"
id="login-bt"
@click="login"
>
<template #default
><span style="font-size: clamp(2em, 1.5vw, 2.8em)"
>&ensp;&ensp;</span
></template
>
</ElButton>
</div>
</div>
</template>
<script lang="ts">
import { getCaptchaSrc, login, verifyCaptcha } from '@/utils/auth'
import backShape from '@/assets/svg/back-shape.svg'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
import { PRODUCTION_NAME } from '@/constants/Common.constants'
export default {
name: 'LoginPage',
data() {
return {
backShape,
captchaSrc: getCaptchaSrc(),
userName: '',
password: '',
captcha: '',
loggingIn: false
}
},
methods: {
PRODUCTION_NAME() {
return PRODUCTION_NAME
},
getNewCaptcha() {
this.captchaSrc = getCaptchaSrc()
},
async login() {
if (!this.userName) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>用户名</strong> 为空'
})
return
}
if (!this.password) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>密码</strong> 为空'
})
return
}
if (!this.captcha) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>验证码</strong> 为空'
})
return
}
if (!verifyCaptcha(this.captcha)) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>验证码</strong> 错误'
})
return
}
if (await login(this.userName, this.password)) {
ElMessage.success({
dangerouslyUseHTMLString: true,
message: '<strong>登录成功</strong>'
})
this.loggingIn = true
const _this = this
setTimeout(function () {
_this.$router.push('/')
}, 1500)
} else {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>用户名</strong> 或 <strong>密码</strong> 错误'
})
}
}
}
}
</script>
<style scoped>
.background {
width: 100vw;
min-width: 900px;
height: 100vh;
min-height: 500px;
background: linear-gradient(to right, #5495f1, #82b5ff);
}
.card-back {
position: absolute;
left: 50%;
top: 50%;
width: 65vw;
min-width: 800px;
height: 25vw;
min-height: 350px;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.6);
transform: translateX(-50%) translateY(-50%);
}
.production-name {
position: absolute;
left: 6%;
top: 8%;
font-size: clamp(2em, 2vw, 2.8em);
color: var(--font-main-color);
}
.production-name .emphasize {
font-size: 2em;
font-weight: bold;
color: var(--main-color);
}
.back-shape {
position: absolute;
height: 80%;
left: 30%;
top: 10%;
}
.card-front {
position: absolute;
left: 50%;
top: 50%;
width: 25vw;
min-width: 350px;
height: 35vw;
min-height: 500px;
border-radius: 15px;
background-color: white;
transform: translateY(-50%);
}
.login-title {
margin-top: 10%;
margin-left: 10%;
font-size: clamp(2.8em, 2.8vw, 4em);
letter-spacing: 0.25em;
font-weight: bold;
color: var(--main-color);
}
.input-box {
display: flex;
flex-direction: row;
background-color: #f0f0f0;
border-radius: 5px;
overflow: hidden;
}
.input-box input {
flex: 1;
font-size: 1.6em;
background-color: transparent;
padding-right: 10px;
color: var(--font-main-color);
}
.user-name-box,
.password-box {
margin: 10% auto -2% auto;
height: 10%;
width: 80%;
}
.captcha-set {
display: flex;
margin: 15% auto 12% auto;
height: 10%;
width: 80%;
}
.captcha-set img {
height: 100%;
cursor: pointer;
}
.captcha-box {
flex: 1;
padding-right: 10px;
height: 100%;
}
.captcha-box input {
padding: 0 10px;
height: 100%;
width: 100%;
}
#login-bt {
display: block;
margin: 0 auto;
width: 80%;
height: auto;
}
</style>

View File

@@ -106,7 +106,7 @@
</div>
<div class="user-info">
<div class="user-name">
<span>用户名</span>
<span>{{ username }}</span>
</div>
<div class="user-desc">
<span>用户介绍</span>
@@ -120,7 +120,7 @@
<el-button style="width: 100%">个人档案</el-button>
</div>
<div>
<el-button style="width: 100%">退出</el-button>
<el-button @click="logout" style="width: 100%">退出</el-button>
</div>
</div>
</template>
@@ -155,9 +155,17 @@ import {
SIZE_ICON_SM
} from '@/constants/Common.constants.js'
import _ from 'lodash'
import { getUsername, logout } from '@/utils/auth'
export default {
name: 'MainFrame',
data() {
return {
routes: _.filter(_.get(this.$router, 'options.routes[0].children'), 'meta.title'),
isCollapsed: false,
username: ''
}
},
methods: {
SIZE_ICON_LG() {
return SIZE_ICON_LG
@@ -176,16 +184,16 @@ export default {
},
COLOR_FONT_MAIN() {
return COLOR_FONT_MAIN
}
},
data() {
return {
routes: _.filter(_.get(this.$router, 'options.routes[0].children'), 'meta.title'),
isCollapsed: false
logout() {
logout()
this.$router.push({ name: 'Login' })
}
},
mounted() {
console.log(this.routes)
getUsername().then((res) => {
this.username = res.toString()
})
}
}
</script>

View File

@@ -1,4 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import { PRODUCTION_NAME } from '@/constants/Common.constants'
import { getLoginStatus } from '@/utils/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -23,8 +25,41 @@ const router = createRouter({
}
}
]
},
{
path: '/login',
component: async () => await import('@/pages/Login.vue'),
name: 'Login',
meta: {
title: '登录'
}
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
from.path !== '' ? next({ path: from.path }) : next('/')
} else {
if (to.meta.title !== '') {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
document.title = `${PRODUCTION_NAME} - ${to.meta.title}`
}
if (getLoginStatus()) {
if (to.name === 'Login') {
next('/')
} else {
next()
}
} else {
if (to.name === 'Login') {
next()
} else {
next('/login')
}
}
}
})
export default router

View File

@@ -0,0 +1,68 @@
import axios, { type AxiosError } from 'axios'
import { getToken, removeToken } from '@/utils/common'
import router from '@/router'
const service = axios.create({
baseURL: 'http://localhost:8621',
timeout: 10000,
withCredentials: false
})
service.interceptors.request.use(
(config) => {
const token = getToken()
if (token != null) {
config.headers.set('token', token)
}
return config
},
async (error) => {
return await Promise.reject(error)
}
)
service.interceptors.response.use(
(response) => {
return response
},
async (error) => {
if (error.response != null) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(`request error: ${error.response.code} - ${error.response.msg}`)
switch (error.response.code) {
case 30010:
removeToken()
await router.push({ name: 'Login' })
}
}
return await Promise.reject(error?.response?.data)
}
)
const request = {
async get<T = any>(url: string, data?: any): Promise<T> {
return await request.request('GET', url, { params: data })
},
async post<T = any>(url: string, data?: any): Promise<T> {
return await request.request('POST', url, { data })
},
async put<T = any>(url: string, data?: any): Promise<T> {
return await request.request('PUT', url, { data })
},
async delete<T = any>(url: string, data?: any): Promise<T> {
return await request.request('DELETE', url, { params: data })
},
async request<T = any>(method = 'GET', url: string, data?: any): Promise<T> {
return await new Promise((resolve, reject) => {
service({ method, url, ...data })
.then((res) => {
resolve(res as unknown as Promise<T>)
})
.catch((e: Error | AxiosError) => {
reject(e)
})
})
}
}
export default request

61
ui/src/utils/auth.ts Normal file
View File

@@ -0,0 +1,61 @@
import type { Captcha } from './common'
import {
getCaptcha,
getLocalStorage,
getToken,
removeLocalStorage,
setLocalStorage,
setToken
} from './common'
import { TOKEN_NAME } from '@/constants/Common.constants'
import _ from 'lodash'
import request from '@/services'
let captcha: Captcha
async function login(username: string, passwd: string): Promise<boolean> {
removeLocalStorage('username')
await request.post('/login', { username, passwd }).then((res: any) => {
const response = res.data
if (response.code === 20010) {
setToken(response.data.token)
}
})
return !_.isEmpty(getToken())
}
function logout(): void {
removeLocalStorage(TOKEN_NAME)
removeLocalStorage('username')
}
function getLoginStatus(): boolean {
return getLocalStorage(TOKEN_NAME) != null
}
async function getUsername(): Promise<string | null> {
if (!_.isEmpty(getLocalStorage('username'))) {
return getLocalStorage('username')
}
let username = ''
await request.get('/userInfo').then((res) => {
username = res.data.data.user.username
})
setLocalStorage('username', username)
return username
}
function getCaptchaSrc(): string {
captcha = getCaptcha(300, 150, 4)
return captcha.base64Src
}
function verifyCaptcha(value: string): boolean {
return captcha.value === value.replace(/\s*/g, '').toUpperCase()
}
export { login, logout, getLoginStatus, getUsername, getCaptchaSrc, verifyCaptcha }

View File

@@ -0,0 +1,153 @@
import { TOKEN_NAME } from '@/constants/Common.constants'
interface Captcha {
value: string
base64Src: string
}
function getQueryVariable(variable: string): string | null {
const query = window.location.search.substring(1)
const vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=')
if (pair[0] === variable) {
return decodeURIComponent(pair[1].replace(/\+/g, ' '))
}
}
return null
}
function setCookie(
name: string,
value: string,
daysToLive: number | null,
path: string | null
): void {
let cookie = name + '=' + encodeURIComponent(value)
if (typeof daysToLive === 'number') {
cookie = `${cookie}; max-age=${daysToLive * 24 * 60 * 60}`
}
if (typeof path === 'string') {
cookie += '; path=' + path
}
document.cookie = cookie
}
function setLocalStorage(name: string, value: string): void {
localStorage.setItem(name, value)
}
function setToken(token: string): void {
setLocalStorage(TOKEN_NAME, token)
}
function getCookie(name: string): string | null {
const cookieArr = document.cookie.split(';')
for (let i = 0; i < cookieArr.length; i++) {
const cookiePair = cookieArr[i].split('=')
if (name === cookiePair[0].trim()) {
return decodeURIComponent(cookiePair[1])
}
}
return null
}
function getLocalStorage(name: string): string | null {
return localStorage.getItem(name)
}
function getToken(): string | null {
return getLocalStorage(TOKEN_NAME)
}
function removeCookie(name: string): void {
document.cookie = name + '=; max-age=0'
}
function removeLocalStorage(name: string): void {
localStorage.removeItem(name)
}
function removeToken(): void {
removeLocalStorage(TOKEN_NAME)
}
function randomInt(start: number, end: number): number {
if (start > end) {
const t = start
start = end
end = t
}
start = Math.ceil(start)
end = Math.floor(end)
return start + Math.floor(Math.random() * (end - start))
}
function randomFloat(start: number, end: number): number {
return start + Math.random() * (end - start)
}
function randomColor(start: number, end: number): string {
return `rgb(${randomInt(start, end)},${randomInt(start, end)},${randomInt(start, end)})`
}
function getCaptcha(width: number, high: number, num: number): Captcha {
const CHARTS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'.split('')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx.rect(0, 0, width, high)
ctx.clip()
ctx.fillStyle = randomColor(200, 250)
ctx.fillRect(0, 0, width, high)
for (let i = 0.05 * width * high; i > 0; i--) {
ctx.fillStyle = randomColor(0, 256)
ctx.fillRect(randomInt(0, width), randomInt(0, high), 1, 1)
}
ctx.font = `${high - 4}px Consolas`
ctx.fillStyle = randomColor(160, 200)
let value = ''
for (let i = 0; i < num; i++) {
const x = ((width - 10) / num) * i + 5
const y = high - 12
const r = Math.PI * randomFloat(-0.12, 0.12)
const ch = CHARTS[randomInt(0, CHARTS.length)]
value += ch
ctx.translate(x, y)
ctx.rotate(r)
ctx.fillText(ch, 0, 0)
ctx.rotate(-r)
ctx.translate(-x, -y)
}
const base64Src = canvas.toDataURL('image/jpg')
return {
value,
base64Src
}
}
export type { Captcha }
export {
getQueryVariable,
getCookie,
getLocalStorage,
getToken,
setCookie,
setLocalStorage,
setToken,
removeCookie,
removeLocalStorage,
removeToken,
getCaptcha
}