mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-05 06:04:35 +08:00
feat: 登录实现
This commit is contained in:
parent
44eb1aa0de
commit
37986510d5
15 changed files with 1553 additions and 12660 deletions
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/1Panel-dev/1Panel/init/session"
|
||||
"github.com/1Panel-dev/1Panel/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/utils/jwt"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -95,12 +96,15 @@ func (u *UserService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo,
|
|||
if sID != "" {
|
||||
c.SetCookie(global.CONF.Session.SessionName, "", -1, "", "", false, false)
|
||||
}
|
||||
session, err := global.SESSION.New(c.Request, global.CONF.Session.SessionName)
|
||||
sessionItem, err := global.SESSION.Get(c.Request, global.CONF.Session.SessionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session.Values[global.CONF.Session.SessionUserKey] = user
|
||||
if err := global.SESSION.Save(c.Request, c.Writer, session); err != nil {
|
||||
sessionItem.Values[global.CONF.Session.SessionUserKey] = session.SessionUser{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
}
|
||||
if err := global.SESSION.Save(c.Request, c.Writer, sessionItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package i18n
|
|||
|
||||
import (
|
||||
"embed"
|
||||
"strings"
|
||||
|
||||
ginI18n "github.com/gin-contrib/i18n"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -20,10 +21,18 @@ func GetMsg(msg string) string {
|
|||
}
|
||||
|
||||
func GetMsgWithMap(msg string, maps map[string]interface{}) string {
|
||||
content := ginI18n.MustGetMessage(&i18n.LocalizeConfig{
|
||||
MessageID: msg,
|
||||
TemplateData: maps,
|
||||
})
|
||||
content := ""
|
||||
if maps == nil {
|
||||
content = ginI18n.MustGetMessage(&i18n.LocalizeConfig{
|
||||
MessageID: msg,
|
||||
})
|
||||
} else {
|
||||
content = ginI18n.MustGetMessage(&i18n.LocalizeConfig{
|
||||
MessageID: msg,
|
||||
TemplateData: maps,
|
||||
})
|
||||
}
|
||||
content = strings.ReplaceAll(content, ": <no value>", "")
|
||||
if content == "" {
|
||||
return msg
|
||||
} else {
|
||||
|
|
|
@ -16,3 +16,8 @@ func Init() {
|
|||
}
|
||||
global.SESSION = cs
|
||||
}
|
||||
|
||||
type SessionUser struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
@ -23,6 +24,7 @@ func Start() {
|
|||
db.Init()
|
||||
migration.Init()
|
||||
validator.Init()
|
||||
gob.Register(session.SessionUser{})
|
||||
session.Init()
|
||||
routers := router.Routers()
|
||||
address := fmt.Sprintf(":%d", global.CONF.System.Port)
|
||||
|
|
|
@ -49,12 +49,7 @@ func (j *JWT) CreateClaims(baseClaims BaseClaims) CustomClaims {
|
|||
}
|
||||
|
||||
func (j *JWT) CreateToken(request CustomClaims) (string, error) {
|
||||
request.RegisteredClaims = jwt.RegisteredClaims{
|
||||
Issuer: global.CONF.JWT.Issuer,
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(global.CONF.JWT.ExpiresTime))),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, &request)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &request)
|
||||
return token.SignedString(j.SigningKey)
|
||||
}
|
||||
|
||||
|
|
13903
frontend/package-lock.json
generated
13903
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,3 @@
|
|||
// * 后端微服务端口名
|
||||
export const PORT1 = '/1panel';
|
||||
export const PORT1 = '9999';
|
||||
export const PORT2 = '/hooks';
|
||||
|
|
|
@ -13,7 +13,6 @@ import { ResultData } from '@/api/interface';
|
|||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
import { checkStatus } from './helper/checkStatus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { GlobalStore } from '@/store';
|
||||
import router from '@/routers';
|
||||
|
||||
/**
|
||||
|
@ -38,25 +37,13 @@ const config = {
|
|||
class RequestHttp {
|
||||
service: AxiosInstance;
|
||||
public constructor(config: AxiosRequestConfig) {
|
||||
// 实例化axios
|
||||
this.service = axios.create(config);
|
||||
|
||||
/**
|
||||
* @description 请求拦截器
|
||||
* 客户端发送请求 -> [请求拦截器] -> 服务器
|
||||
* token校验(JWT) : 接受服务器返回的token,存储到vuex/pinia/本地储存当中
|
||||
*/
|
||||
this.service.interceptors.request.use(
|
||||
(config: AxiosRequestConfig) => {
|
||||
const globalStore = GlobalStore();
|
||||
// * 将当前请求添加到 pending 中
|
||||
axiosCanceler.addPending(config);
|
||||
// * 如果当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { headers: { noLoading: true } }来控制不显示loading,参见loginApi
|
||||
config.headers!.noLoading || showFullScreenLoading();
|
||||
const token: string = globalStore.token;
|
||||
return {
|
||||
...config,
|
||||
headers: { ...config.headers, 'x-access-token': token },
|
||||
};
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
|
@ -71,14 +58,12 @@ class RequestHttp {
|
|||
this.service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data, config } = response;
|
||||
const globalStore = GlobalStore();
|
||||
// * 在请求结束后,移除本次请求,并关闭请求 loading
|
||||
axiosCanceler.removePending(config);
|
||||
tryHideFullScreenLoading();
|
||||
// * 登陆失效(code == 599)
|
||||
if (data.code == ResultEnum.OVERDUE) {
|
||||
ElMessage.error(data.msg);
|
||||
globalStore.setToken('');
|
||||
router.replace({
|
||||
path: '/login',
|
||||
});
|
||||
|
@ -108,20 +93,16 @@ class RequestHttp {
|
|||
}
|
||||
|
||||
// * 常用请求方法封装
|
||||
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
get(url: string, params?: object, _object = {}): Promise<ResultData> {
|
||||
return this.service.get(url, { params, ..._object });
|
||||
}
|
||||
post<T>(
|
||||
url: string,
|
||||
params?: object,
|
||||
_object = {},
|
||||
): Promise<ResultData<T>> {
|
||||
post(url: string, params?: object, _object = {}): Promise<ResultData> {
|
||||
return this.service.post(url, params, _object);
|
||||
}
|
||||
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
put(url: string, params?: object, _object = {}): Promise<ResultData> {
|
||||
return this.service.put(url, params, _object);
|
||||
}
|
||||
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
|
||||
delete(url: string, params?: any, _object = {}): Promise<ResultData> {
|
||||
return this.service.delete(url, { params, ..._object });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// * 请求响应参数(不包含data)
|
||||
export interface Result {
|
||||
code: string;
|
||||
msg: string;
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// * 请求响应参数(包含data)
|
||||
export interface ResultData<T = any> extends Result {
|
||||
data?: T;
|
||||
export interface ResultData {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// * 分页响应参数
|
||||
|
@ -31,11 +33,25 @@ export interface CommonModel {
|
|||
// * 登录模块
|
||||
export namespace Login {
|
||||
export interface ReqLoginForm {
|
||||
username: string;
|
||||
name: string;
|
||||
password: string;
|
||||
captcha: string;
|
||||
captchaID: string;
|
||||
authMethod: string;
|
||||
}
|
||||
export interface ResLogin {
|
||||
access_token: string;
|
||||
code: number;
|
||||
data: logInfo;
|
||||
message: string;
|
||||
}
|
||||
export interface logInfo {
|
||||
name: string;
|
||||
token: string;
|
||||
}
|
||||
export interface ResCaptcha {
|
||||
imagePath: string;
|
||||
captchaID: string;
|
||||
captchaLength: number;
|
||||
}
|
||||
export interface ResAuthButtons {
|
||||
[propName: string]: any;
|
||||
|
|
|
@ -1,31 +1,10 @@
|
|||
import { Login } from '@/api/interface/index';
|
||||
import { PORT1 } from '@/api/config/servicePort';
|
||||
// import qs from 'qs';
|
||||
|
||||
import http from '@/api';
|
||||
|
||||
/**
|
||||
* @name 登录模块
|
||||
*/
|
||||
// * 用户登录接口
|
||||
export const loginApi = (params: Login.ReqLoginForm) => {
|
||||
console.log(params);
|
||||
return http.post<Login.ResLogin>(PORT1 + `base/login`, params); // 正常 post json 请求 ==> application/json
|
||||
// return http.post<Login.ResLogin>(PORT1 + `/login`, {}, { params }); // post 请求携带 query 参数 ==> ?username=admin&password=123456
|
||||
// return http.post<Login.ResLogin>(PORT1 + `/login`, qs.stringify(params)); // post 请求携带 表单 参数 ==> application/x-www-form-urlencoded
|
||||
// return http.post<Login.ResLogin>(PORT1 + `/login`, params, {
|
||||
// headers: { noLoading: true },
|
||||
// }); // 控制当前请求不显示 loading
|
||||
|
||||
// return { data: { access_token: '565656565' } };
|
||||
return http.post(`/auth/login`, params);
|
||||
};
|
||||
|
||||
// // * 获取按钮权限
|
||||
// export const getAuthButtons = () => {
|
||||
// return http.get<Login.ResAuthButtons>(PORT1 + `/auth/buttons`);
|
||||
// };
|
||||
|
||||
// * 获取验证码
|
||||
export const getCaptcha = () => {
|
||||
return http.post<Login.ResCaptcha>(PORT1 + `base/captcha`);
|
||||
return http.get(`/auth/captcha`);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ router.beforeEach((to, from, next) => {
|
|||
|
||||
// * 判断是否有Token
|
||||
const globalStore = GlobalStore();
|
||||
if (!globalStore.token) {
|
||||
if (!globalStore.isLogin) {
|
||||
next({
|
||||
path: '/login',
|
||||
});
|
||||
|
|
|
@ -4,57 +4,37 @@ import { createPinia } from 'pinia';
|
|||
import piniaPersistConfig from '@/config/piniaPersist';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
|
||||
// defineStore 调用后返回一个函数,调用该函数获得 Store 实体
|
||||
export const GlobalStore = defineStore({
|
||||
// id: 必须的,在所有 Store 中唯一
|
||||
id: 'GlobalState',
|
||||
// state: 返回对象的函数
|
||||
state: (): GlobalState => ({
|
||||
// token
|
||||
token: '',
|
||||
// userInfo
|
||||
isLogin: false,
|
||||
userInfo: '',
|
||||
// element组件大小
|
||||
assemblySize: 'default',
|
||||
// language
|
||||
language: '',
|
||||
// themeConfig
|
||||
themeConfig: {
|
||||
// 默认 primary 主题颜色
|
||||
primary: '#409EFF',
|
||||
// 深色模式
|
||||
isDark: false,
|
||||
// 灰色模式
|
||||
isGrey: false,
|
||||
// 色弱模式
|
||||
isWeak: false,
|
||||
// 面包屑导航
|
||||
breadcrumb: true,
|
||||
// 标签页
|
||||
tabs: false,
|
||||
// 页脚
|
||||
footer: true,
|
||||
},
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// setToken
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
setLogStatus(login: boolean) {
|
||||
this.isLogin = login;
|
||||
},
|
||||
// setUserInfo
|
||||
setUserInfo(userInfo: any) {
|
||||
this.userInfo = userInfo;
|
||||
},
|
||||
// setAssemblySizeSize
|
||||
setAssemblySizeSize(assemblySize: string) {
|
||||
this.assemblySize = assemblySize;
|
||||
},
|
||||
// updateLanguage
|
||||
updateLanguage(language: string) {
|
||||
this.language = language;
|
||||
},
|
||||
// setThemeConfig
|
||||
setThemeConfig(themeConfig: ThemeConfigProp) {
|
||||
this.themeConfig = themeConfig;
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface ThemeConfigProp {
|
|||
|
||||
/* GlobalState */
|
||||
export interface GlobalState {
|
||||
token: string;
|
||||
isLogin: boolean;
|
||||
userInfo: any;
|
||||
assemblySize: string;
|
||||
language: string;
|
||||
|
|
|
@ -1,39 +1,78 @@
|
|||
<template>
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" placeholder="用户名:admin / user">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<user />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.name"
|
||||
placeholder="用户名:admin / user"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<user />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
type="password"
|
||||
v-model="loginForm.password"
|
||||
placeholder="密码:123456"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-form-item prop="captcha">
|
||||
<div class="vPicBox">
|
||||
<el-input
|
||||
v-model="loginForm.captcha"
|
||||
placeholder="请输入验证码"
|
||||
style="width: 60%"
|
||||
/>
|
||||
<div class="vPic">
|
||||
<img
|
||||
v-if="captcha.imagePath"
|
||||
:src="captcha.imagePath"
|
||||
alt="请输入验证码"
|
||||
@click="loginVerify()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input type="password" v-model="loginForm.password" placeholder="密码:123456" show-password autocomplete="new-password">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<lock />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="login-btn">
|
||||
<el-button :icon="CircleClose" round @click="resetForm(loginFormRef)" size="large">重置</el-button>
|
||||
<el-button :icon="UserFilled" round @click="login(loginFormRef)" size="large" type="primary" :loading="loading">
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="login-btn">
|
||||
<el-button round @click="resetForm(loginFormRef)" size="large"
|
||||
>重置</el-button
|
||||
>
|
||||
<el-button
|
||||
round
|
||||
@click="login(loginFormRef)"
|
||||
size="large"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Login,getCaptcha } from '@/api/interface';
|
||||
import { Login } from '@/api/interface';
|
||||
import type { ElForm } from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { loginApi } from '@/api/modules/login';
|
||||
import { loginApi, getCaptcha } from '@/api/modules/login';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { MenuStore } from '@/store/modules/menu';
|
||||
|
||||
|
@ -50,13 +89,22 @@ const loginRules = reactive({
|
|||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive<Login.ReqLoginForm>({
|
||||
name: '',
|
||||
password: '',
|
||||
name: 'admin',
|
||||
password: 'Songliu123++',
|
||||
captcha: '',
|
||||
captchaID: '',
|
||||
authMethod: '',
|
||||
});
|
||||
|
||||
const captcha = reactive<Login.ResCaptcha>({
|
||||
captchaID: '',
|
||||
imagePath: '',
|
||||
captchaLength: 0,
|
||||
});
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const router = useRouter();
|
||||
// login
|
||||
|
||||
const login = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
|
@ -64,29 +112,42 @@ const login = (formEl: FormInstance | undefined) => {
|
|||
loading.value = true;
|
||||
try {
|
||||
const requestLoginForm: Login.ReqLoginForm = {
|
||||
name: loginForm.username,
|
||||
name: loginForm.name,
|
||||
password: loginForm.password,
|
||||
captcha: loginForm.captcha,
|
||||
captchaID: captcha.captchaID,
|
||||
authMethod: '',
|
||||
};
|
||||
const res = await loginApi(requestLoginForm);
|
||||
// * 存储 token
|
||||
globalStore.setToken(res.data!.access_token);
|
||||
// * 登录成功之后清除上个账号的 menulist 和 tabs 数据
|
||||
globalStore.setUserInfo(res.data.name);
|
||||
globalStore.setLogStatus(true);
|
||||
menuStore.setMenuList([]);
|
||||
|
||||
ElMessage.success('登录成功!');
|
||||
router.push({ name: 'home' });
|
||||
} catch (error) {
|
||||
loginVerify();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// resetForm
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
};
|
||||
|
||||
const loginVerify = () => {
|
||||
getCaptcha().then(async (ele) => {
|
||||
captcha.imagePath = ele.data?.imagePath ? ele.data.imagePath : '';
|
||||
captcha.captchaID = ele.data?.captchaID ? ele.data.captchaID : '';
|
||||
captcha.captchaLength = ele.data?.captchaLength
|
||||
? ele.data.captchaLength
|
||||
: 0;
|
||||
});
|
||||
};
|
||||
loginVerify();
|
||||
|
||||
onMounted(() => {
|
||||
// 监听enter事件(调用登录)
|
||||
document.onkeydown = (e: any) => {
|
||||
|
@ -105,4 +166,20 @@ onMounted(() => {
|
|||
|
||||
<style scoped lang="scss">
|
||||
@import '../index.scss';
|
||||
|
||||
.vPicBox {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
.vPic {
|
||||
width: 33%;
|
||||
height: 38px;
|
||||
background: #ccc;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,24 +36,18 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
},
|
||||
},
|
||||
},
|
||||
// server config
|
||||
server: {
|
||||
host: '0.0.0.0', // 服务器主机名,如果允许外部访问,可设置为"0.0.0.0"
|
||||
port: viteEnv.VITE_PORT,
|
||||
open: viteEnv.VITE_OPEN,
|
||||
cors: true,
|
||||
// https: false,
|
||||
// 代理跨域(mock 不需要配置,这里只是个事列)
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: "https://www.fastmock.site/mock/f81e8333c1a9276214bcdbc170d9e0a0", // fastmock
|
||||
target: 'https://mock.mengxuegu.com/mock/629d727e6163854a32e8307e', // easymock
|
||||
target: 'http://localhost:9999',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// plugins
|
||||
plugins: [
|
||||
vue(),
|
||||
createHtmlPlugin({
|
||||
|
|
Loading…
Add table
Reference in a new issue