19 Commits

Author SHA1 Message Date
Harry-zklcdc 42de9caf52 [Feat] Add Docs Link 2025-03-06 00:26:00 +08:00
Harry-zklcdc 5780f84714 [Feat] Set logging model to "main" in main function 2025-03-05 18:59:42 +08:00
Harry-zklcdc de8c601be9 [Fix] 🐛 Instance Mount Path 2025-03-04 18:21:57 +08:00
Harry-zklcdc e283a626a1 [Fix] 🐛 Photo Show Error 2025-02-28 21:26:33 +08:00
Harry-zklcdc 37ba1baf71 [Fix] 😅 Typo #15 2025-02-25 00:02:57 +08:00
Harry-zklcdc 81b52e80b5 [Fix] 🐛 Fix Regexp Pattern to Support subdomain Email #15 2025-02-24 23:38:04 +08:00
Harry-zklcdc 13bd350274 [Fix] 🐛 Add Sendding Status #15 2025-02-24 22:12:25 +08:00
Harry-zklcdc 35a55a9e03 [Feat] Add ForgetPassword API & Pages #15 2025-02-24 18:05:38 +08:00
Harry-zklcdc 801335ab5d [Feat] Add Same Email ErrorRespone at ResetEmail API #15 2025-02-24 17:18:22 +08:00
Harry-zklcdc d0e95f9521 [Feat] Reset Email API & Pages #15 2025-02-24 17:15:14 +08:00
Harry-zklcdc 417397f282 [Refactor] ♻️ Change ResetPassword API URL #15 2025-02-24 16:44:51 +08:00
Harry-zklcdc e80f3f02aa [Feat] Add ResetPassword Page #15 2025-02-24 16:40:52 +08:00
Harry-zklcdc 2e3cfbcecb [Feat] Add Email Verify API & Pages #15 2025-02-24 16:29:30 +08:00
Harry-zklcdc 52bdfa17eb [Fix] 😅 Typo 2025-02-24 01:20:02 +08:00
Harry-zklcdc cbc426c180 [Feat] Unified Entrance of Web Services 2025-02-23 22:14:27 +08:00
Harry-zklcdc 1a3a126388 [Fix] 😅 Typo 2025-02-23 18:57:53 +08:00
Harry-zklcdc 1133650632 [Docs] 📝 Add VideoDemo to README.md 2025-02-23 17:20:29 +08:00
Harry-zklcdc 9d561cb63a [Fix] 🐛 Images Key Init 2025-02-19 02:14:29 +08:00
Harry-zklcdc 1f55ecb649 [Fix] 🐛 Cors Error at FrontEnd 2025-02-16 23:53:14 +08:00
41 changed files with 1135 additions and 60 deletions
+8 -5
View File
@@ -38,7 +38,7 @@
✅ 资源性能监控看板 ✅ 资源性能监控看板
✅ Jupter Notebook 支持 ✅ Jupyter Notebook 支持
## 📝 安装 & 使用教程 ## 📝 安装 & 使用教程
@@ -46,12 +46,15 @@
> [!WARNING] > [!WARNING]
> 部署仓库: [XShengTech/MEGREZ-Deploy](https://github.com/XShengTech/MEGREZ-Deploy) > 部署仓库: [XShengTech/MEGREZ-Deploy](https://github.com/XShengTech/MEGREZ-Deploy)
> >
> 查看文档 [**>>> 🚧 正在施工中 <<<**]() > 查看文档 [**>>> MEGREZ 文档 <<<**](http://docs.megrez.xsheng-ai.com/)
## 📌 效果展示 ## 📌 效果展示
### 视频演示
[Bilibili - MEGREZ——你的新一代开源GPU管理系统](https://www.bilibili.com/video/BV1EnfWY9ECC/)
### 登录注册 ### 登录注册
| 登录 | 注册 | | 登录 | 注册 |
@@ -76,9 +79,9 @@
### 内置功能 ### 内置功能
| VSCode 网页版 | Jupter Notebook | Grafana 资源监控 | | VSCode 网页版 | Jupyter Notebook | Grafana 资源监控 |
| ---------------------------------------------------- | ----------------------------------------------- | -------------------------------- | | ---------------------------------------------------- | ----------------------------------------------- | -------------------------------- |
| ![image-20250116010708107](./assets/code-server.png) | ![Jupter NoteBook](./assets/juper-notebook.png) | ![Grafana](./assets/grafana.png) | | ![image-20250116010708107](./assets/code-server.png) | ![Jupyter NoteBook](./assets/juper-notebook.png) | ![Grafana](./assets/grafana.png) |
### 系统管理 ### 系统管理
+7
View File
@@ -10,6 +10,13 @@ database:
redis: redis:
host: ms-redis host: ms-redis
port: 6379 port: 6379
smtp:
host:
port:
user:
password:
ssl: false
system: system:
base_url:
verify: false verify: false
mount_dir: /path/to/mount mount_dir: /path/to/mount
+18
View File
@@ -35,6 +35,24 @@ export default {
UserRegister(data) { UserRegister(data) {
return ajax('user/register', 'post', { data }) return ajax('user/register', 'post', { data })
}, },
UserForgetRequest(data) {
return ajax('user/forget', 'post', { data })
},
UserForgerPassword(data) {
return ajax('user/password', 'put', { data })
},
UserResetPassword(data) {
return ajax('user/password', 'post', { data })
},
UserResetEmail(data) {
return ajax('user/email', 'post', { data })
},
UserVerifyRequest() {
return ajax(`user/verify`, 'post', {})
},
UserVerify(code) {
return ajax(`user/verify/${code}`, 'get', {})
},
UserInstancesList(params) { UserInstancesList(params) {
return ajax('user/instances', 'get', { params }) return ajax('user/instances', 'get', { params })
+1 -7
View File
@@ -56,15 +56,9 @@ const model = ref([
{ {
label: '使用文档', label: '使用文档',
icon: 'pi pi-fw pi-book text-amber-500', icon: 'pi pi-fw pi-book text-amber-500',
url: '#', url: 'http://docs.megrez.xsheng-ai.com/guide/usage/',
target: '_blank' target: '_blank'
}, },
// {
// label: '开源信息',
// icon: 'pi pi-fw pi-cog',
// url: 'https://github.com/primefaces/sakai-vue',
// target: '_blank'
// },
] ]
} }
]); ]);
+18
View File
@@ -1,8 +1,11 @@
import AppLayout from '@/layout/AppLayout.vue'; import AppLayout from '@/layout/AppLayout.vue';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import Forget from '@/views/Forget.vue';
import Login from '@/views/Login.vue'; import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue'; import Register from '@/views/Register.vue';
import Reset from '@/views/Reset.vue';
import Verify from '@/views/Verify.vue';
import InstanceCreate from '@/views/users/InstanceCreate.vue'; import InstanceCreate from '@/views/users/InstanceCreate.vue';
import InstanceList from '@/views/users/InstanceList.vue'; import InstanceList from '@/views/users/InstanceList.vue';
@@ -32,6 +35,21 @@ const router = createRouter({
name: 'register', name: 'register',
component: Register component: Register
}, },
{
path: '/forget',
name: 'forget',
component: Forget
},
{
path: '/reset/:code',
name: 'forget-reset',
component: Reset
},
{
path: '/verify/:code',
name: 'verify',
component: Verify
},
{ {
path: '/', path: '/',
name: 'dashboard', name: 'dashboard',
+76
View File
@@ -0,0 +1,76 @@
<template>
<FloatingConfigurator />
<div
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
<div class="flex flex-col items-center justify-center">
<div
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
<div class="text-center items-center flex flex-col mb-8">
<img :src="logo" style="width: 16rem;" />
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mt-6 mb-4">天权 算能聚联计算平台</div>
<span class="text-muted-color font-medium">忘记密码</span>
</div>
<div>
<label for="email" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">邮箱</label>
<InputText id="email" type="text" placeholder="邮箱" class="w-full md:w-[30rem] mb-8" v-model="form.email" />
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">记起密码<span
class="text-primary" @click="handleLogin">立即登入</span></span>
</div>
<Button v-if="!requesting" label="发送邮件" class="w-full" @click="handleSubmit"></Button>
<Button v-else label="发送中" icon="pi pi-spin pi-spinner" disabled class="w-full"></Button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import logo from '@/assets/logo-text.webp';
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
import api from '@/api';
import { useToast } from 'primevue/usetoast';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter()
const toast = useToast()
const requesting = ref(false)
const form = ref({
email: ''
})
const handleSubmit = () => {
requesting.value = true
api.UserForgetRequest(form.value).then(res => {
toast.add({ severity: 'success', summary: '发送成功', detail: '请查看邮箱', life: 3000 })
requesting.value = false
}).catch(err => {
console.error(err)
toast.add({ severity: 'error', summary: '发送失败', detail: '请检查后重新尝试', life: 3000 })
requesting.value = false
})
}
const handleLogin = () => {
router.push('/login')
}
</script>
<style scoped>
.pi-eye {
transform: scale(1.6);
margin-right: 1rem;
}
.pi-eye-slash {
transform: scale(1.6);
margin-right: 1rem;
}
</style>
+6 -1
View File
@@ -25,7 +25,8 @@
<div class="flex items-center justify-between mt-2 mb-8 gap-8"> <div class="flex items-center justify-between mt-2 mb-8 gap-8">
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">没有账号<span <span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">没有账号<span
class="text-primary" @click="handleRegister">立即注册</span></span> class="text-primary" @click="handleRegister">立即注册</span></span>
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary">忘记密码</span> <span class="font-medium no-underline ml-2 text-right cursor-pointer text-primary"
@click="handleForget">忘记密码</span>
</div> </div>
<Button label="登入" class="w-full" @click="handleSubmit"></Button> <Button label="登入" class="w-full" @click="handleSubmit"></Button>
</div> </div>
@@ -69,6 +70,10 @@ const handleSubmit = () => {
const handleRegister = () => { const handleRegister = () => {
router.push('/register') router.push('/register')
} }
const handleForget = () => {
router.push('/forget')
}
</script> </script>
<style scoped> <style scoped>
+102
View File
@@ -0,0 +1,102 @@
<template>
<FloatingConfigurator />
<div
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
<div class="flex flex-col items-center justify-center">
<div
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
<div class="text-center items-center flex flex-col mb-8">
<img :src="logo" style="width: 16rem;" />
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mt-6 mb-4">天权 算能聚联计算平台</div>
<span class="text-muted-color font-medium">重置密码</span>
</div>
<div>
<label for="password"
class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">新密码</label>
<Password id="password" v-model="form.password" placeholder="新密码" :toggleMask="true"
class="w-full md:w-[30rem] mb-4" fluid :feedback="false" />
<label for="repassword"
class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">重复密码</label>
<Password id="repassword" v-model="form.repassword" placeholder="重复密码" :toggleMask="true" class="mb-4" fluid
:feedback="false" @keydown.enter="handleSubmit" />
<div class="flex items-center justify-between mt-2 mb-8 gap-8">
<span class="font-medium no-underline ml-2 text-right cursor-pointer text-slate-600">记起密码<span
class="text-primary" @click="handleLogin">立即登入</span></span>
</div>
<Button label="重置密码" class="w-full" @click="handleSubmit"></Button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import logo from '@/assets/logo-text.webp';
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
import api from '@/api';
import { useToast } from 'primevue/usetoast';
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const router = useRouter()
const route = useRoute()
const toast = useToast()
const form = ref({
code: route.params.code,
password: '',
repassword: ''
})
const handleSubmit = () => {
if (!form.value.code) {
toast.add({ severity: 'error', summary: '验证码错误', detail: '请检查后重新尝试', life: 3000 })
return
}
if (!form.value.password) {
toast.add({ severity: 'error', summary: '密码不能为空', detail: '请检查后重新尝试', life: 3000 })
return
}
if (!form.value.repassword) {
toast.add({ severity: 'error', summary: '重复密码不能为空', detail: '请检查后重新尝试', life: 3000 })
return
}
if (form.value.password !== form.value.repassword) {
toast.add({ severity: 'error', summary: '两次密码不一致', detail: '请检查后重新尝试', life: 3000 })
return
}
api.UserForgerPassword(form.value).then(res => {
toast.add({ severity: 'success', summary: '重置密码成功', detail: '请登录', life: 3000 })
router.push('/login')
}).catch(err => {
console.error(err)
toast.add({ severity: 'error', summary: '重置密码失败', detail: '请检查后重新尝试', life: 3000 })
})
}
const handleLogin = () => {
router.push('/login')
}
</script>
<style scoped>
.pi-eye {
transform: scale(1.6);
margin-right: 1rem;
}
.pi-eye-slash {
transform: scale(1.6);
margin-right: 1rem;
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<FloatingConfigurator />
<div class="flex items-center justify-center min-h-screen overflow-hidden">
<div class="flex flex-col items-center justify-center">
<img :src="logo" width="64" height="32" class="-mt-6 mb-4" />
<div
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, color-mix(in srgb, var(--primary-color), transparent 60%) 10%, var(--surface-ground) 30%)">
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20 flex flex-col items-center"
style="border-radius: 53px">
<h1 class="text-surface-900 dark:text-surface-0 font-bold text-3xl lg:text-5xl mb-2">邮箱验证</h1>
<div class="text-surface-600 dark:text-surface-200 mb-8">验证邮箱地址</div>
<router-link v-if="status == 0"
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
<span class="flex justify-center items-center border-2 border-primary text-primary rounded-border"
style="height: 3.5rem; width: 3.5rem">
<i class="pi pi-fw pi-spin pi-spinner !text-2xl"></i>
</span>
<span class="ml-6 flex flex-col">
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
验证中
</span>
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
请稍后
</span>
</span>
</router-link>
<router-link v-else-if="status == 1" to="/"
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
<span class="flex justify-center items-center border-2 border-emerald-500 text-emerald-500 rounded-border"
style="height: 3.5rem; width: 3.5rem">
<i class="pi pi-fw pi-check !text-2xl"></i>
</span>
<span class="ml-6 flex flex-col">
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
验证成功
</span>
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
你可以开始使用了
</span>
</span>
</router-link>
<router-link v-else-if="status == 2" to="/"
class="w-full flex items-center mb-8 py-8 border-surface-300 dark:border-surface-500 border-b">
<span class="flex justify-center items-center border-2 border-red-500 text-red-500 rounded-border"
style="height: 3.5rem; width: 3.5rem">
<i class="pi pi-fw pi-times !text-2xl"></i>
</span>
<span class="ml-6 flex flex-col">
<span class="text-surface-900 dark:text-surface-0 lg:text-xl font-medium mb-0">
验证失败
</span>
<span class="text-surface-600 dark:text-surface-200 lg:text-xl">
请重新验证
</span>
</span>
</router-link>
<Button as="router-link" label="回到仪表盘" to="/" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import logo from '@/assets/logo.svg';
import FloatingConfigurator from '@/components/FloatingConfigurator.vue';
import api from '@/api';
import { useToast } from 'primevue/usetoast';
import { ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const toast = useToast()
const status = ref(0) // 0: verifing, 1: success, 2: fail
const code = route.params.code
api.UserVerify(code).then(res => {
status.value = 1
toast.add({ severity: 'success', summary: '验证成功', detail: '你可以开始使用了', life: 3000 })
}).catch(err => {
status.value = 2
toast.add({ severity: 'error', summary: '验证失败', detail: err.response.data.msg, life: 3000 })
console.error(err)
})
</script>
+5 -6
View File
@@ -70,12 +70,11 @@
:href="'http://' + data.code_server_address" target="_blank" v-tooltip.top="'VSCode Web'" /> :href="'http://' + data.code_server_address" target="_blank" v-tooltip.top="'VSCode Web'" />
<Button v-else icon="pi pi-code" aria-label="Filter" v-tooltip.top="'VSCode Web'" disabled /> <Button v-else icon="pi pi-code" aria-label="Filter" v-tooltip.top="'VSCode Web'" disabled />
<Button v-if="data.status == statusRunning" severity="info" icon="pi pi-inbox" aria-label="Filter" as="a" <Button v-if="data.status == statusRunning" severity="info" icon="pi pi-inbox" aria-label="Filter" as="a"
:href="'http://' + data.jupyter_address + '/lab'" target="_blank" v-tooltip.top="'Jupter Lab'" /> :href="'http://' + data.jupyter_address" target="_blank" v-tooltip.top="'Jupyter Lab'" />
<Button v-else severity="info" icon="pi pi-inbox" aria-label="Filter" v-tooltip.top="'Jupter Lab'" <Button v-else severity="info" icon="pi pi-inbox" aria-label="Filter" v-tooltip.top="'Jupyter Lab'"
disabled /> disabled />
<Button v-if="data.status == statusRunning" severity="contrast" icon="pi pi-chart-bar" as="a" <Button v-if="data.status == statusRunning" severity="contrast" icon="pi pi-chart-bar" as="a"
:href="'http://' + data.grafana_address + '/public-dashboards/2c510f203876465ba76617510ce3e219'" :href="'http://' + data.grafana_address" target="_blank" v-tooltip.top="'监控'" />
target="_blank" v-tooltip.top="'监控'" />
<Button v-else severity="contrast" icon="pi pi-chart-bar" v-tooltip.top="'监控'" disabled /> <Button v-else severity="contrast" icon="pi pi-chart-bar" v-tooltip.top="'监控'" disabled />
<Button v-if="!isAdmin" icon="pi pi-ellipsis-h" severity="secondary" aria-label="Bookmark" <Button v-if="!isAdmin" icon="pi pi-ellipsis-h" severity="secondary" aria-label="Bookmark"
@click="showMenu($event, data)" /> @click="showMenu($event, data)" />
@@ -122,13 +121,13 @@
</Fieldset> </Fieldset>
<Fieldset legend="GPU"> <Fieldset legend="GPU">
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.gpu_type }} * {{ instanceDetail.gpu_count <span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.gpu_type }} * {{ instanceDetail.gpu_count
}}</span> }}</span>
<span v-else>无卡模式</span> <span v-else>无卡模式</span>
</Fieldset> </Fieldset>
<div class="flex flex-col md:flex-row gap-4"> <div class="flex flex-col md:flex-row gap-4">
<Fieldset class="flex flex-wrap gap-2 w-full" legend="CPU"> <Fieldset class="flex flex-wrap gap-2 w-full" legend="CPU">
<span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.cpu_count_per_gpu * instanceDetail.gpu_count <span v-if="instanceDetail.gpu_count !== 0">{{ instanceDetail.cpu_count_per_gpu * instanceDetail.gpu_count
}} }}
</span> </span>
<span v-else>1 </span> <span v-else>1 </span>
</Fieldset> </Fieldset>
+3 -4
View File
@@ -74,12 +74,11 @@
:href="'http://' + data.code_server_address" target="_blank" v-tooltip.top="'VSCode Web'" /> :href="'http://' + data.code_server_address" target="_blank" v-tooltip.top="'VSCode Web'" />
<Button v-else icon="pi pi-code" aria-label="Filter" v-tooltip.top="'VSCode Web'" disabled /> <Button v-else icon="pi pi-code" aria-label="Filter" v-tooltip.top="'VSCode Web'" disabled />
<Button v-if="data.status == statusRunning" severity="info" icon="pi pi-inbox" aria-label="Filter" as="a" <Button v-if="data.status == statusRunning" severity="info" icon="pi pi-inbox" aria-label="Filter" as="a"
:href="'http://' + data.jupyter_address + '/lab'" target="_blank" v-tooltip.top="'Jupter Lab'" /> :href="'http://' + data.jupyter_address" target="_blank" v-tooltip.top="'Jupyter Lab'" />
<Button v-else severity="info" icon="pi pi-inbox" aria-label="Filter" v-tooltip.top="'Jupter Lab'" <Button v-else severity="info" icon="pi pi-inbox" aria-label="Filter" v-tooltip.top="'Jupyter Lab'"
disabled /> disabled />
<Button v-if="data.status == statusRunning" severity="contrast" icon="pi pi-chart-bar" as="a" <Button v-if="data.status == statusRunning" severity="contrast" icon="pi pi-chart-bar" as="a"
:href="'http://' + data.grafana_address + '/public-dashboards/2c510f203876465ba76617510ce3e219'" :href="'http://' + data.grafana_address" target="_blank" v-tooltip.top="'监控'" />
target="_blank" v-tooltip.top="'监控'" />
<Button v-else severity="contrast" icon="pi pi-chart-bar" v-tooltip.top="'监控'" disabled /> <Button v-else severity="contrast" icon="pi pi-chart-bar" v-tooltip.top="'监控'" disabled />
<Button icon="pi pi-ellipsis-h" severity="secondary" aria-label="Bookmark" <Button icon="pi pi-ellipsis-h" severity="secondary" aria-label="Bookmark"
@click="showMenu($event, data)" /> @click="showMenu($event, data)" />
+175 -2
View File
@@ -1,3 +1,176 @@
<template> <template>
个人设置 <SectionBanner label="安全设置" icon="pi pi-cog text-emerald-500"></SectionBanner>
</template> <Fluid>
<div class="flex flex-col md:flex-row gap-8">
<div class="md:w-1/2">
<div class="card rounded-2xl flex flex-col gap-4">
<div class="font-semibold text-xl">邮箱验证</div>
<div class="flex flex-col gap-2">
<label for="email">邮箱</label>
<InputText v-model="userProfile.email" type="text" disabled />
</div>
<div class="mt-2">
<Button v-if="!userProfile.verify && !verifyRequesting" label="验证" severity="warn"
style="width: 5.6rem; float: right;" @click="emailVerify" />
<Button v-else-if="verifyRequesting" label="发送中" severity="warn" icon="pi pi-spin pi-spinner" disabled
style="width: 7.2rem; float: right;" />
<Button v-else label="已验证" severity="secondary" disabled style="width: 5.6rem; float: right;" />
</div>
</div>
<div class="card rounded-2xl flex flex-col gap-4">
<div class="font-semibold text-xl">修改邮箱</div>
<div class="flex flex-col gap-2">
<label for="email">邮箱</label>
<InputText v-if="!resetEmailModifyStatus" v-model="userProfile.email" type="text" disabled />
<InputText v-else v-model="resetEmailData.email" type="text" />
</div>
<div v-if="!resetEmailModifyStatus" class="mt-2">
<Button label="修改" style="width: 5.6rem; float: right;" @click="resetEmailModifyStatus = true" />
</div>
<div v-else>
<Button v-if="!resetEmailRequesting" label="保存" style="width: 5.6rem; float: right;" @click="resetEmail" />
<Button v-else label="保存中" icon="pi pi-spin pi-spinner" disabled style="width: 7.2rem; float: right;" />
<Button class="mr-2" label="取消" severity="secondary" style="width: 5.6rem; float: right;"
@click="resetEmailModifyStatus = false" />
</div>
</div>
</div>
<div class="md:w-1/2">
<div class="card rounded-2xl flex flex-col gap-4">
<div class="font-semibold text-xl">修改密码</div>
<div class="flex flex-col gap-2">
<label for="old_password">原密码</label>
<Password v-model="resetPasswordData.old_password" :toggleMask="true" :feedback="false" />
</div>
<div class="flex flex-col gap-2">
<label for="new_password">新密码</label>
<Password v-model="resetPasswordData.new_password" :toggleMask="true" :feedback="false" />
</div>
<div class="flex flex-col gap-2">
<label for="re_password">重复密码</label>
<Password v-model="resetPasswordData.re_password" :toggleMask="true" :feedback="false" />
</div>
<div class="mt-2">
<Button v-if="!resetPasswordRequesting" label="保存" style="width: 5.6rem; float: right;"
@click="resetPassword" />
<Button v-else label="保存中" icon="pi pi-spin pi-spinner" disabled style="width: 7.2rem; float: right;" />
</div>
</div>
</div>
</div>
</Fluid>
</template>
<script setup>
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
import api from '@/api';
const toast = useToast()
const userProfile = ref({})
const verifyRequesting = ref(false)
const resetEmailModifyStatus = ref(false)
const resetEmailRequesting = ref(false)
const resetEmailData = ref({
email: ''
})
const resetPasswordRequesting = ref(false)
const resetPasswordData = ref({
old_password: '',
new_password: '',
re_password: ''
})
const getProfile = () => {
api.GetUserProfile().then((res) => {
userProfile.value = res.data.data.result
resetEmailData.value.email = userProfile.value.email
console.log(userProfile.value)
}).catch((err) => {
toast.add({ severity: 'error', summary: '获取用户信息失败', detail: err.response.data.msg, life: 3000 })
console.error(err)
})
}
const emailVerify = () => {
verifyRequesting.value = true
api.UserVerifyRequest().then((res) => {
toast.add({ severity: 'success', summary: '验证邮件已发送', detail: '请前往邮箱查看', life: 3000 })
verifyRequesting.value = false
}).catch((err) => {
toast.add({ severity: 'error', summary: '发送失败', detail: err.response.data.msg, life: 3000 })
verifyRequesting.value = false
console.error(err)
})
}
const resetPassword = () => {
resetPasswordRequesting.value = true
if (resetPasswordData.value.old_password == "") {
toast.add({ severity: 'error', summary: '原密码不能为空', detail: '请重新输入', life: 3000 })
resetPasswordRequesting.value = false
return
}
if (resetPasswordData.value.new_password == "") {
toast.add({ severity: 'error', summary: '新密码不能为空', detail: '请重新输入', life: 3000 })
resetPasswordRequesting.value = false
return
}
if (resetPasswordData.value.re_password == "") {
toast.add({ severity: 'error', summary: '重复密码不能为空', detail: '请重新输入', life: 3000 })
resetPasswordRequesting.value = false
return
}
if (resetPasswordData.value.new_password != resetPasswordData.value.re_password) {
toast.add({ severity: 'error', summary: '两次密码不一致', detail: '请重新输入', life: 3000 })
resetPasswordRequesting.value = false
return
}
api.UserResetPassword(resetPasswordData.value).then((res) => {
toast.add({ severity: 'success', summary: '修改成功', detail: '请重新登录', life: 3000 })
resetPasswordRequesting.value = false
}).catch((err) => {
toast.add({ severity: 'error', summary: '修改失败', detail: err.response.data.msg, life: 3000 })
resetPasswordRequesting.value = false
console.error(err)
})
}
const resetEmail = () => {
resetEmailRequesting.value = true
if (resetEmailData.value.email == '') {
toast.add({ severity: 'error', summary: '邮箱不能为空', detail: '请重新输入', life: 3000 })
resetEmailRequesting.value = false
return
}
api.UserResetEmail(resetEmailData.value).then((res) => {
toast.add({ severity: 'success', summary: '修改成功', detail: '请重新登录', life: 3000 })
resetEmailRequesting.value = false
resetEmailModifyStatus.value = false
getProfile()
}).catch((err) => {
toast.add({ severity: 'error', summary: '修改失败', detail: err.response.data.msg, life: 3000 })
resetEmailRequesting.value = false
console.error(err)
})
}
onMounted(() => {
getProfile()
})
</script>
+14
View File
@@ -0,0 +1,14 @@
package utils
import (
"regexp"
)
func EmailFormat(email string) bool {
pattern := `\w[-\w.+]*@([-A-Za-z0-9]+\.)+[A-Za-z]{2,14}`
match, err := regexp.MatchString(pattern, email)
if err != nil {
return false
}
return match
}
+1
View File
@@ -27,6 +27,7 @@ var (
) )
func main() { func main() {
l.SetModel("main")
l.Info("Branch: %s", BRANCH) l.Info("Branch: %s", BRANCH)
l.Info("Version: %s", VERSION) l.Info("Version: %s", VERSION)
l.Info("Commit: %s", COMMIT) l.Info("Commit: %s", COMMIT)
+2 -1
View File
@@ -16,7 +16,8 @@ type Users struct {
Password string `json:"password,omitempty" gorm:"type:varchar(255);not null"` Password string `json:"password,omitempty" gorm:"type:varchar(255);not null"`
Role int `json:"role" gorm:"not null,default:0"` Role int `json:"role" gorm:"not null,default:0"`
Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"` Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"`
Verify bool `json:"verify" gorm:"not null,default:false"`
Balance float64 `json:"balance" gorm:"not null"` Balance float64 `json:"balance" gorm:"not null"`
+1 -1
View File
@@ -20,7 +20,7 @@ func detailHandler(ctx iris.Context) {
user := models.Users{ user := models.Users{
ID: id, ID: id,
} }
result := database.DB.Select("id", "username", "email", "role", "balance", "created_at").First(&user) result := database.DB.Select("id", "username", "email", "role", "verify", "balance", "created_at").First(&user)
if result.Error != nil { if result.Error != nil {
l.Error("detail user error: %v", result.Error) l.Error("detail user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeAdminUserDetailError, iris.StatusInternalServerError) middleware.Error(ctx, middleware.CodeAdminUserDetailError, iris.StatusInternalServerError)
+1 -1
View File
@@ -42,7 +42,7 @@ func listHandler(ctx iris.Context) {
return return
} }
result := database.DB.Limit(limit).Offset(offset).Select("id", "username", "email", "role", "balance", "created_at").Order("id").Find(&users) result := database.DB.Limit(limit).Offset(offset).Select("id", "username", "email", "role", "verify", "balance", "created_at").Order("id").Find(&users)
if result.Error != nil { if result.Error != nil {
l.Error("list users error: %v", result.Error) l.Error("list users error: %v", result.Error)
middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError) middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError)
+13
View File
@@ -1,6 +1,7 @@
package users package users
import ( import (
"megrez/libs/utils"
"megrez/models" "megrez/models"
"megrez/routers/api/v1/middleware" "megrez/routers/api/v1/middleware"
"megrez/services/database" "megrez/services/database"
@@ -9,8 +10,10 @@ import (
) )
type modifyReqStruct struct { type modifyReqStruct struct {
Email *string `json:"email"`
Password *string `json:"password"` Password *string `json:"password"`
Role *int `json:"role"` Role *int `json:"role"`
Verify *bool `json:"verify"`
} }
func modifyHandler(ctx iris.Context) { func modifyHandler(ctx iris.Context) {
@@ -45,6 +48,12 @@ func modifyHandler(ctx iris.Context) {
return return
} }
if req.Email != nil {
if *req.Email != "" && !utils.EmailFormat(*req.Email) {
user.Email = *req.Email
}
}
if req.Password != nil { if req.Password != nil {
if *req.Password != "" { if *req.Password != "" {
user.Password = user.PasswordHash(*req.Password) user.Password = user.PasswordHash(*req.Password)
@@ -55,6 +64,10 @@ func modifyHandler(ctx iris.Context) {
user.Role = *req.Role user.Role = *req.Role
} }
if req.Verify != nil {
user.Verify = *req.Verify
}
result = database.DB.Save(&user) result = database.DB.Save(&user)
if result.Error != nil { if result.Error != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
+12
View File
@@ -14,6 +14,12 @@ const (
CodeUserNotExist ResCode = 1002 CodeUserNotExist ResCode = 1002
CodeRegisterRequestError ResCode = 1003 CodeRegisterRequestError ResCode = 1003
CodeRegisterError ResCode = 1004 CodeRegisterError ResCode = 1004
CodeEmailFormatError ResCode = 1005
CodeUserAlreadyVerified ResCode = 1006
CodeUserVerifyInvalid ResCode = 1007
CodePasswordNotMatch ResCode = 1008
CodeEmailSameError ResCode = 1009
CodeInternalCreateError ResCode = 1010 CodeInternalCreateError ResCode = 1010
CodeInstanceDeleteError ResCode = 1011 CodeInstanceDeleteError ResCode = 1011
CodeInstanceQueryError ResCode = 1012 CodeInstanceQueryError ResCode = 1012
@@ -56,6 +62,12 @@ var codeMsgMap = map[ResCode]string{
CodeUserNotExist: "user not exist", CodeUserNotExist: "user not exist",
CodeRegisterRequestError: "register request error", CodeRegisterRequestError: "register request error",
CodeRegisterError: "username or email exist", CodeRegisterError: "username or email exist",
CodeEmailFormatError: "email format error",
CodeUserAlreadyVerified: "user already verified",
CodeUserVerifyInvalid: "email verify error",
CodePasswordNotMatch: "password not match",
CodeEmailSameError: "email same error",
CodeInternalCreateError: "create error", CodeInternalCreateError: "create error",
CodeInstanceDeleteError: "delete instance error", CodeInstanceDeleteError: "delete instance error",
CodeInstanceStatusError: "instance status error", CodeInstanceStatusError: "instance status error",
+78
View File
@@ -0,0 +1,78 @@
package user
import (
"megrez/models"
"megrez/routers/api/v1/middleware"
"megrez/services/database"
"megrez/services/redis"
"github.com/kataras/iris/v12"
)
type forgerPasswordStruct struct {
Code string `json:"code"`
Password string `json:"password"`
RePassword string `json:"repassword"`
}
func forgetPasswordHandler(ctx iris.Context) {
l.SetFunction("forgetPasswordHandler")
var req forgerPasswordStruct
if err := ctx.ReadJSON(&req); err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
if req.Code == "" || req.Password == "" || req.RePassword == "" {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
if req.Password != req.RePassword {
middleware.Error(ctx, middleware.CodePasswordNotMatch, iris.StatusBadRequest)
return
}
rdb := redis.RawDB
v := rdb.Get(ctx, forgetPasswordRedisKeyPrefix+req.Code)
if v.Err() != nil {
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
return
}
r := rdb.Del(ctx, forgetPasswordRedisKeyPrefix+req.Code)
if r.Err() != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("delete redis verify code error: %v", r.Err())
return
}
id, err := v.Int()
if err != nil {
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
return
}
user := models.Users{
ID: uint(id),
}
result := database.DB.First(&user)
if result.Error != nil {
l.Error("get user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
return
}
user.Password = user.PasswordHash(req.Password)
result = database.DB.Model(&user).Update("password", user.Password)
if result.Error != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("update user password Error: %v", result.Error)
return
}
middleware.Success(ctx)
}
+97
View File
@@ -0,0 +1,97 @@
package user
import (
"fmt"
"megrez/libs/crypto"
"megrez/models"
"megrez/routers/api/v1/middleware"
"megrez/services/config"
"megrez/services/database"
"megrez/services/redis"
"megrez/services/smtp"
"time"
"github.com/kataras/iris/v12"
)
type forgetSendStruct struct {
Email string `json:"email"`
}
const forgetPasswordRedisKeyPrefix = "forget:user:"
const forgetPasswordUrlPrefix = "/reset/"
const forgetPasswordTitle = "重置密码"
const forgetPasswordHTMLFormat = `
<div>
<table cellpadding="0" align="center" style="overflow:hidden;background:#fff;margin:0 auto;text-align:left;position:relative;font-size:14px; font-family:'lucida Grande',Verdana;line-height:1.5;box-shadow:0 0 3px #ccc;border:1px solid #ccc;border-radius:5px;border-collapse:collapse;">
<tbody>
<tr>
<th valign="middle" style="height:38px;color:#fff; font-size:14px;line-height:38px; font-weight:bold;text-align:left;padding:10px 24px 6px; border-bottom:1px solid #467ec3;background:#518bcb;border-radius:5px 5px 0 0;">
MEGREZ 天权算能聚联计算平台</th>
</tr>
<tr>
<td>
<div style="padding:20px 35px 40px;">
<h2 style="font-weight:bold;margin-bottom:5px;font-size:14px;">Hello, %s:</h2>
<p style="margin-top:20px">
请在15分钟内点击链接: <a href="%s">%s</a> &nbsp;进行密码重置操作,十五分钟后该链接将会失效.
</p>
<p style="margin-top:20px">
为了保护你的账户,请不要使用单一的密码来进行重置。
</p>
<p style="margin-top:20px">
如果您有任何问题,请联系系统管理员以获得更多信息与支持。
</p>
<p style="margin-left:2em;"></p>
<p style="text-indent:0;text-align:right;">MEGREZ 天权算能聚联计算平台</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
`
func forgetSendHandler(ctx iris.Context) {
var req forgetSendStruct
if err := ctx.ReadJSON(&req); err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
if req.Email == "" {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
user := models.Users{
Email: req.Email,
}
result := database.DB.Where(&user).First(&user)
if result.Error != nil {
l.Error("get user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError)
return
}
rdb := redis.RawDB
forgetUrl := crypto.Hex(32)
err := rdb.Set(ctx, forgetPasswordRedisKeyPrefix+forgetUrl, user.ID, 15*time.Minute).Err()
if err != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("Set Redis error: %v", err)
return
}
forgetUrl = config.GetSystemBaseUrl() + forgetPasswordUrlPrefix + forgetUrl
err = smtp.Send(user.Email, forgetPasswordTitle, fmt.Sprintf(forgetPasswordHTMLFormat, user.Username, forgetUrl, forgetUrl))
if err != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("Send SMTP Error: %v", err)
return
}
middleware.Success(ctx)
}
+1
View File
@@ -65,6 +65,7 @@ func loginHandler(ctx iris.Context) {
Email: user.Email, Email: user.Email,
Role: user.Role, Role: user.Role,
Balance: user.Balance, Balance: user.Balance,
Verify: user.Verify,
} }
middleware.Result(ctx, profile) middleware.Result(ctx, profile)
+2
View File
@@ -14,6 +14,7 @@ type profile struct {
Email string `json:"email"` Email string `json:"email"`
Role int `json:"role"` Role int `json:"role"`
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
Verify bool `json:"verify"`
} }
func profileHandler(ctx iris.Context) { func profileHandler(ctx iris.Context) {
@@ -40,6 +41,7 @@ func profileHandler(ctx iris.Context) {
Email: user.Email, Email: user.Email,
Role: user.Role, Role: user.Role,
Balance: user.Balance, Balance: user.Balance,
Verify: user.Verify,
} }
middleware.Result(ctx, profile) middleware.Result(ctx, profile)
+6
View File
@@ -1,6 +1,7 @@
package user package user
import ( import (
"megrez/libs/utils"
"megrez/models" "megrez/models"
"megrez/routers/api/v1/middleware" "megrez/routers/api/v1/middleware"
"megrez/services/config" "megrez/services/config"
@@ -29,6 +30,11 @@ func registerHandler(ctx iris.Context) {
return return
} }
if !utils.EmailFormat(userReq.Email) {
middleware.Error(ctx, middleware.CodeEmailFormatError, iris.StatusBadRequest)
return
}
user := models.Users{ user := models.Users{
Username: userReq.Username, Username: userReq.Username,
Email: userReq.Email, Email: userReq.Email,
+64
View File
@@ -0,0 +1,64 @@
package user
import (
"megrez/libs/utils"
"megrez/models"
"megrez/routers/api/v1/middleware"
"megrez/services/database"
"github.com/kataras/iris/v12"
)
type resetEmailStruct struct {
Email string `json:"email"`
}
func resetEmailHandler(ctx iris.Context) {
l.SetFunction("resetEmailHandler")
userId, err := ctx.Values().GetInt("userId")
if err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
var req resetEmailStruct
if err := ctx.ReadJSON(&req); err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
if req.Email == "" {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
if !utils.EmailFormat(req.Email) {
middleware.Error(ctx, middleware.CodeEmailFormatError, iris.StatusBadRequest)
return
}
user := models.Users{
ID: uint(userId),
}
result := database.DB.First(&user)
if result.Error != nil {
l.Error("get user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError)
return
}
if req.Email == user.Email {
middleware.Error(ctx, middleware.CodeEmailSameError, iris.StatusBadRequest)
return
}
result = database.DB.Model(&user).Update("email", req.Email).Update("verify", false)
if result.Error != nil {
l.Error("save user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeInternalPatchError, iris.StatusInternalServerError)
return
}
middleware.Success(ctx)
}
+10 -4
View File
@@ -11,6 +11,7 @@ import (
type resetPasswordStruct struct { type resetPasswordStruct struct {
OldPassword string `json:"old_password"` OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"` NewPassword string `json:"new_password"`
RePassword string `json:"re_password"`
} }
func resetPasswordHandler(ctx iris.Context) { func resetPasswordHandler(ctx iris.Context) {
@@ -22,12 +23,17 @@ func resetPasswordHandler(ctx iris.Context) {
return return
} }
var resetPassword resetPasswordStruct var req resetPasswordStruct
if err := ctx.ReadJSON(&resetPassword); err != nil { if err := ctx.ReadJSON(&req); err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return return
} }
if req.NewPassword != req.RePassword {
middleware.Error(ctx, middleware.CodePasswordNotMatch, iris.StatusBadRequest)
return
}
user := models.Users{ user := models.Users{
ID: uint(userId), ID: uint(userId),
} }
@@ -38,12 +44,12 @@ func resetPasswordHandler(ctx iris.Context) {
return return
} }
if !user.CheckPassword(resetPassword.OldPassword) { if !user.CheckPassword(req.OldPassword) {
middleware.Error(ctx, middleware.CodePasswordError, iris.StatusBadRequest) middleware.Error(ctx, middleware.CodePasswordError, iris.StatusBadRequest)
return return
} }
user.Password = user.PasswordHash(resetPassword.NewPassword) user.Password = user.PasswordHash(req.NewPassword)
result = database.DB.Save(&user) result = database.DB.Save(&user)
if result.Error != nil { if result.Error != nil {
l.Error("save user error: %v", result.Error) l.Error("save user error: %v", result.Error)
+6 -1
View File
@@ -24,7 +24,12 @@ func InitUser(party router.Party) {
party.Get("/logout", middleware.AuthCheck, logoutHandler) party.Get("/logout", middleware.AuthCheck, logoutHandler)
party.Post("/register", registerHandler) party.Post("/register", registerHandler)
party.Get("/profile", middleware.AuthCheck, profileHandler) party.Get("/profile", middleware.AuthCheck, profileHandler)
party.Post("/resetPassword", middleware.AuthCheck, resetPasswordHandler) party.Post("/password", middleware.AuthCheck, resetPasswordHandler)
party.Post("/forget", forgetSendHandler)
party.Put("/password", forgetPasswordHandler)
party.Post("/email", middleware.AuthCheck, resetEmailHandler)
party.Get("/verify/{code:string}", verifyHandler)
party.Post("/verify", middleware.AuthCheck, verifySendHandler)
servers.InitServers(party.Party("/servers")) servers.InitServers(party.Party("/servers"))
instances.InitInstances(party.Party("/instances")) instances.InitInstances(party.Party("/instances"))
+51
View File
@@ -0,0 +1,51 @@
package user
import (
"megrez/models"
"megrez/routers/api/v1/middleware"
"megrez/services/database"
"megrez/services/redis"
"github.com/kataras/iris/v12"
)
func verifyHandler(ctx iris.Context) {
l.SetFunction("verifyHandler")
code := ctx.Params().GetString("code")
rdb := redis.RawDB
v := rdb.Get(ctx, verifyRedisKeyPrefix+code)
if v.Err() != nil {
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
return
}
r := rdb.Del(ctx, verifyRedisKeyPrefix+code)
if r.Err() != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("delete redis verify code error: %v", r.Err())
return
}
email := v.Val()
user := models.Users{
Email: email,
}
result := database.DB.Where(&user).First(&user)
if result.Error != nil {
l.Error("get user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeUserVerifyInvalid, iris.StatusBadRequest)
return
}
result = database.DB.Model(&user).Update("verify", true)
if result.Error != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("update user verify status Error: %v", result.Error)
return
}
middleware.Success(ctx)
}
+92
View File
@@ -0,0 +1,92 @@
package user
import (
"fmt"
"megrez/libs/crypto"
"megrez/models"
"megrez/routers/api/v1/middleware"
"megrez/services/config"
"megrez/services/database"
"megrez/services/redis"
"megrez/services/smtp"
"time"
"github.com/kataras/iris/v12"
)
const verifyRedisKeyPrefix = "verify:user:"
const verifyUrlPrefix = "/verify/"
const verifyTitle = "邮箱验证"
const verifyHTMLFormat = `
<div>
<table cellpadding="0" align="center" style="overflow:hidden;background:#fff;margin:0 auto;text-align:left;position:relative;font-size:14px; font-family:'lucida Grande',Verdana;line-height:1.5;box-shadow:0 0 3px #ccc;border:1px solid #ccc;border-radius:5px;border-collapse:collapse;">
<tbody>
<tr>
<th valign="middle" style="height:38px;color:#fff; font-size:14px;line-height:38px; font-weight:bold;text-align:left;padding:10px 24px 6px; border-bottom:1px solid #467ec3;background:#518bcb;border-radius:5px 5px 0 0;">
MEGREZ 天权算能聚联计算平台</th>
</tr>
<tr>
<td>
<div style="padding:20px 35px 40px;">
<h2 style="font-weight:bold;margin-bottom:5px;font-size:14px;">Hello, %s:</h2>
<p style="margin-top:20px">
请在15分钟内点击链接: <a href="%s">%s</a> &nbsp;进行邮箱验证操作,十五分钟后该链接将会失效.
</p>
<p style="margin-top:20px">
如果您有任何问题,请联系系统管理员以获得更多信息与支持。
</p>
<p style="margin-left:2em;"></p>
<p style="text-indent:0;text-align:right;">MEGREZ 天权算能聚联计算平台</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
`
func verifySendHandler(ctx iris.Context) {
l.SetFunction("verifySendHandler")
userId, err := ctx.Values().GetInt("userId")
if err != nil {
middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest)
return
}
user := models.Users{
ID: uint(userId),
}
result := database.DB.First(&user)
if result.Error != nil {
l.Error("get user error: %v", result.Error)
middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError)
return
}
if user.Verify {
middleware.Error(ctx, middleware.CodeUserAlreadyVerified, iris.StatusBadRequest)
return
}
rdb := redis.RawDB
verifyUrl := crypto.Hex(32)
err = rdb.Set(ctx, verifyRedisKeyPrefix+verifyUrl, user.Email, 15*time.Minute).Err()
if err != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("Set Redis Error: %v", err)
return
}
verifyUrl = config.GetSystemBaseUrl() + verifyUrlPrefix + verifyUrl
err = smtp.Send(user.Email, verifyTitle, fmt.Sprintf(verifyHTMLFormat, user.Username, verifyUrl, verifyUrl))
if err != nil {
middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError)
l.Error("Send SMTP Error: %v", err)
return
}
middleware.Success(ctx)
}
+12
View File
@@ -0,0 +1,12 @@
package index
import "github.com/kataras/iris/v12"
func cors(ctx iris.Context) {
ctx.Header("Access-Control-Allow-Origin", "*")
ctx.Header("Access-Control-Allow-Methods", "GET")
ctx.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
ctx.Header("Access-Control-Max-Age", "86400")
ctx.Header("Access-Control-Allow-Credentials", "true")
ctx.Next()
}
+1
View File
@@ -5,6 +5,7 @@ import (
) )
func InitIndex(app *iris.Application) { func InitIndex(app *iris.Application) {
app.Use(cors)
app.HandleDir("/", GetWebFS(), iris.DirOptions{ app.HandleDir("/", GetWebFS(), iris.DirOptions{
IndexName: "/index.html", IndexName: "/index.html",
Compress: true, Compress: true,
+11
View File
@@ -9,6 +9,7 @@ type configStruct struct {
Http httpStruct `yaml:"http,omitempty"` Http httpStruct `yaml:"http,omitempty"`
Database databaseStruct `yaml:"database,omitempty"` Database databaseStruct `yaml:"database,omitempty"`
Redis redisStruct `yaml:"redis,omitempty"` Redis redisStruct `yaml:"redis,omitempty"`
Smtp smtpStruct `yaml:"smtp,omitempty"`
Log logStruct `yaml:"log,omitempty"` Log logStruct `yaml:"log,omitempty"`
System systemStruct `yaml:"system,omitempty"` System systemStruct `yaml:"system,omitempty"`
} }
@@ -35,12 +36,21 @@ type redisStruct struct {
SentinelPassword string `yaml:"sentinel_password,omitempty"` SentinelPassword string `yaml:"sentinel_password,omitempty"`
} }
type smtpStruct struct {
Host string `yaml:"host,omitempty"`
Port int `yaml:"port,omitempty"`
Password string `yaml:"password,omitempty"`
User string `yaml:"user,omitempty"`
SSL bool `yaml:"ssl,omitempty"`
}
type logStruct struct { type logStruct struct {
Level string `yaml:"level,omitempty"` Level string `yaml:"level,omitempty"`
File string `yaml:"file,omitempty"` File string `yaml:"file,omitempty"`
} }
type systemStruct struct { type systemStruct struct {
BaseUrl string `yaml:"base_url,omitempty"`
Salt string `yaml:"salt,omitempty"` Salt string `yaml:"salt,omitempty"`
Verify bool `yaml:"verify,omitempty"` Verify bool `yaml:"verify,omitempty"`
MountDir string `yaml:"mount_dir,omitempty"` MountDir string `yaml:"mount_dir,omitempty"`
@@ -64,6 +74,7 @@ var config = configStruct{
Password: "GpuManager", Password: "GpuManager",
Database: 0, Database: 0,
}, },
Smtp: smtpStruct{},
Log: logStruct{ Log: logStruct{
Level: "DEBUG", Level: "DEBUG",
File: "data/logs/backend.log", File: "data/logs/backend.log",
+13
View File
@@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"strconv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -19,6 +20,10 @@ func InitConfig(path string) {
l.Fatal("Failed to unmarshal config file", err) l.Fatal("Failed to unmarshal config file", err)
} }
l.SetLevel(config.GetLogLevel()) l.SetLevel(config.GetLogLevel())
if config.System.BaseUrl == "" {
config.System.BaseUrl = "http://" + config.Http.Host + ":" + strconv.Itoa(config.Http.Port)
}
} }
func GetDatabase() databaseStruct { func GetDatabase() databaseStruct {
@@ -33,6 +38,10 @@ func GetRedis() redisStruct {
return config.GetRedis() return config.GetRedis()
} }
func GetSmtp() smtpStruct {
return config.GetSmtp()
}
func GetLogLevel() string { func GetLogLevel() string {
return config.GetLogLevel() return config.GetLogLevel()
} }
@@ -41,6 +50,10 @@ func GetLogFile() string {
return config.GetLogFile() return config.GetLogFile()
} }
func GetSystemBaseUrl() string {
return config.GetSystemBaseUrl()
}
func GetSystemSalt() string { func GetSystemSalt() string {
return config.GetSystemSalt() return config.GetSystemSalt()
} }
+8
View File
@@ -20,6 +20,10 @@ func (c *configStruct) GetRedis() redisStruct {
return c.Redis return c.Redis
} }
func (c *configStruct) GetSmtp() smtpStruct {
return c.Smtp
}
func (c *configStruct) GetLogLevel() string { func (c *configStruct) GetLogLevel() string {
return c.Log.Level return c.Log.Level
} }
@@ -28,6 +32,10 @@ func (c *configStruct) GetLogFile() string {
return c.Log.File return c.Log.File
} }
func (c *configStruct) GetSystemBaseUrl() string {
return c.System.BaseUrl
}
func (c *configStruct) GetSystemSalt() string { func (c *configStruct) GetSystemSalt() string {
return c.System.Salt return c.System.Salt
} }
+4 -4
View File
@@ -48,7 +48,7 @@ func Create(instance *models.Instances) (containerName, volumeName string, err e
} }
go func() { go func() {
SetJupterPassword(server.IP, server.Port, server.Apikey, containerName, instance.SshPasswd) SetJupyterPassword(server.IP, server.Port, server.Apikey, containerName, instance.SshPasswd)
SetCodeServerPassword(server.IP, server.Port, server.Apikey, containerName, instance.SshPasswd) SetCodeServerPassword(server.IP, server.Port, server.Apikey, containerName, instance.SshPasswd)
}() }()
@@ -64,9 +64,9 @@ func Create(instance *models.Instances) (containerName, volumeName string, err e
instance.SshAddress = server.IP + ":" + portBindings["22"] instance.SshAddress = server.IP + ":" + portBindings["22"]
instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"]
instance.JupyterAddress = server.IP + ":" + portBindings["8888"] instance.JupyterAddress = server.IP + ":" + portBindings["80"] + "/jupyter"
instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] instance.GrafanaAddress = server.IP + ":" + portBindings["80"] + "/monitor/public-dashboards/2c510f203876465ba76617510ce3e219"
instance.CodeServerAddress = server.IP + ":" + portBindings["8080"] instance.CodeServerAddress = server.IP + ":" + portBindings["80"] + "/code-server/"
instance.Status = 0 instance.Status = 0
result = database.DB.Save(&instance) result = database.DB.Save(&instance)
@@ -40,10 +40,8 @@ func createInstance(ip string, port int, apikey string,
Memory: strconv.Itoa(memorySize) + "GB", Memory: strconv.Itoa(memorySize) + "GB",
ContainerPorts: []string{ ContainerPorts: []string{
"22", // SSH "22", // SSH
"80", // Nginx
"6007", // TensorBoard "6007", // TensorBoard
"8888", // Jupyter Notebook
"3000", // Grafana
"8080", // Code-Server
"34567", // Custom Port "34567", // Custom Port
}, },
Env: []string{ Env: []string{
@@ -64,7 +62,7 @@ func createInstance(ip string, port int, apikey string,
if config.GetSystemMountDir() != "" { if config.GetSystemMountDir() != "" {
data.Binds = append(data.Binds, bindStruct{ data.Binds = append(data.Binds, bindStruct{
Src: config.GetSystemMountDir(), Src: config.GetSystemMountDir(),
Dest: "/root/megrez-pub", Dest: "/root/megrez-mnt",
}) })
} }
+8 -8
View File
@@ -57,9 +57,9 @@ func SetRootPassword(ip string, port int, apikey string,
return nil return nil
} }
func SetJupterPassword(ip string, port int, apikey string, func SetJupyterPassword(ip string, port int, apikey string,
containerName, password string) (err error) { containerName, password string) (err error) {
l.SetFunction("SetJupterPassword") l.SetFunction("SetJupyterPassword")
// Set Jupyter Password // Set Jupyter Password
data := executeReq{ data := executeReq{
@@ -85,8 +85,8 @@ func SetJupterPassword(ip string, port int, apikey string,
c.Do() c.Do()
if c.GetStatusCode() != 200 { if c.GetStatusCode() != 200 {
l.Error("set jupter password error: %d", c.GetStatusCode()) l.Error("set jupyter password error: %d", c.GetStatusCode())
return errors.New("set jupter password request error") return errors.New("set jupyter password request error")
} }
var res resStruct var res resStruct
@@ -97,7 +97,7 @@ func SetJupterPassword(ip string, port int, apikey string,
} }
if res.Code != 200 { if res.Code != 200 {
l.Error("set jupter password code: %d, error: %s", res.Code, res.Msg) l.Error("set jupyter password code: %d, error: %s", res.Code, res.Msg)
return errors.New(res.Msg) return errors.New(res.Msg)
} }
@@ -124,8 +124,8 @@ func SetJupterPassword(ip string, port int, apikey string,
c.Do() c.Do()
if c.GetStatusCode() != 200 { if c.GetStatusCode() != 200 {
l.Error("restart jupter error: %d", c.GetStatusCode()) l.Error("restart jupyter error: %d", c.GetStatusCode())
return errors.New("restart jupter request error") return errors.New("restart jupyter request error")
} }
err = json.Unmarshal(c.GetBody(), &res) err = json.Unmarshal(c.GetBody(), &res)
@@ -135,7 +135,7 @@ func SetJupterPassword(ip string, port int, apikey string,
} }
if res.Code != 200 { if res.Code != 200 {
l.Error("restart jupter code: %d, error: %s", res.Code, res.Msg) l.Error("restart jupyter code: %d, error: %s", res.Code, res.Msg)
return errors.New(res.Msg) return errors.New(res.Msg)
} }
+8 -7
View File
@@ -92,7 +92,7 @@ func Patch(instance *models.Instances, gpuCount, volumeSize int, cpuOnly bool) (
} }
go func() { go func() {
SetJupterPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) SetJupyterPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd)
SetCodeServerPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) SetCodeServerPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd)
}() }()
@@ -104,8 +104,9 @@ func Patch(instance *models.Instances, gpuCount, volumeSize int, cpuOnly bool) (
instance.SshAddress = server.IP + ":" + portBindings["22"] instance.SshAddress = server.IP + ":" + portBindings["22"]
instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"]
instance.JupyterAddress = server.IP + ":" + portBindings["8888"] instance.JupyterAddress = server.IP + ":" + portBindings["80"] + "/jupyter"
instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] instance.GrafanaAddress = server.IP + ":" + portBindings["80"] + "/monitor/public-dashboards/2c510f203876465ba76617510ce3e219"
instance.CodeServerAddress = server.IP + ":" + portBindings["80"] + "/code-server/"
instance.CpuOnly = true instance.CpuOnly = true
instance.GpuCount = 0 instance.GpuCount = 0
@@ -136,7 +137,7 @@ func Patch(instance *models.Instances, gpuCount, volumeSize int, cpuOnly bool) (
} }
go func() { go func() {
SetJupterPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) SetJupyterPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd)
SetCodeServerPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) SetCodeServerPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd)
}() }()
@@ -148,9 +149,9 @@ func Patch(instance *models.Instances, gpuCount, volumeSize int, cpuOnly bool) (
instance.SshAddress = server.IP + ":" + portBindings["22"] instance.SshAddress = server.IP + ":" + portBindings["22"]
instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"]
instance.JupyterAddress = server.IP + ":" + portBindings["8888"] instance.JupyterAddress = server.IP + ":" + portBindings["80"] + "/jupyter"
instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] instance.GrafanaAddress = server.IP + ":" + portBindings["80"] + "/monitor/public-dashboards/2c510f203876465ba76617510ce3e219"
instance.CodeServerAddress = server.IP + ":" + portBindings["8080"] instance.CodeServerAddress = server.IP + ":" + portBindings["80"] + "/code-server/"
instance.CpuOnly = false instance.CpuOnly = false
instance.GpuCount = gpuCount instance.GpuCount = gpuCount
+4 -4
View File
@@ -45,7 +45,7 @@ func Restart(instance *models.Instances) (err error) {
} }
go func() { go func() {
SetJupterPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) SetJupyterPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd)
SetCodeServerPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) SetCodeServerPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd)
}() }()
@@ -57,9 +57,9 @@ func Restart(instance *models.Instances) (err error) {
instance.SshAddress = server.IP + ":" + portBindings["22"] instance.SshAddress = server.IP + ":" + portBindings["22"]
instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"]
instance.JupyterAddress = server.IP + ":" + portBindings["8888"] instance.JupyterAddress = server.IP + ":" + portBindings["80"] + "/jupyter"
instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] instance.GrafanaAddress = server.IP + ":" + portBindings["80"] + "/monitor/public-dashboards/2c510f203876465ba76617510ce3e219"
instance.CodeServerAddress = server.IP + ":" + portBindings["8080"] instance.CodeServerAddress = server.IP + ":" + portBindings["80"] + "/code-server/"
instance.Status = models.InstanceStatusRunning instance.Status = models.InstanceStatusRunning
result = database.DB.Save(&instance) result = database.DB.Save(&instance)
+90
View File
@@ -0,0 +1,90 @@
package smtp
import (
"crypto/tls"
"errors"
"fmt"
"megrez/services/config"
"net/mail"
"net/smtp"
"strconv"
)
const emailTitle = "MEGREZ 天权算能聚联计算平台"
func Send(toAddr, title string, html string) error {
smtpConf := config.GetSmtp()
if smtpConf.Host == "" || smtpConf.Port == 0 || smtpConf.User == "" || smtpConf.Password == "" {
return errors.New("SMTP smtpConfiguration is not set")
}
if !smtpConf.SSL {
auth := smtp.PlainAuth("", smtpConf.User, smtpConf.Password, smtpConf.Host)
msg := append([]byte("Subject: "+title+" - "+emailTitle+" \r\n"+
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\r\n\r\n"),
html...)
err := smtp.SendMail(smtpConf.Host+":"+strconv.Itoa(smtpConf.Port), auth, smtpConf.User, []string{toAddr}, msg)
if err != nil {
return errors.New("fail to send email, Error:" + err.Error())
}
return nil
}
from := mail.Address{Name: "MEGREZ", Address: smtpConf.User}
to := mail.Address{Name: "", Address: toAddr}
auth := smtp.PlainAuth("", smtpConf.User, smtpConf.Password, smtpConf.Host)
// Setup headers
headers := make(map[string]string)
headers["From"] = from.String()
headers["To"] = to.String()
headers["Subject"] = title + " - " + emailTitle
headers["Content-Type"] = "text/html;charset=UTF-8"
msg := ""
for k, v := range headers {
msg += fmt.Sprintf("%s: %s\r\n", k, v)
}
msg += "\r\n" + html
tlssmtpConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: smtpConf.Host,
}
conn, err := tls.Dial("tcp", smtpConf.Host+":"+strconv.Itoa(smtpConf.Port), tlssmtpConfig)
if err != nil {
return errors.New("fail to connect to the server, Error:" + err.Error())
}
c, err := smtp.NewClient(conn, smtpConf.Host)
if err != nil {
return errors.New("fail to create smtp client, Error:" + err.Error())
}
if err = c.Auth(auth); err != nil {
return errors.New("fail to auth, Error:" + err.Error())
}
// To && From
if err = c.Mail(from.Address); err != nil {
return errors.New("fail to set from address, Error:" + err.Error())
}
if err = c.Rcpt(to.Address); err != nil {
return errors.New("fail to set to address, Error:" + err.Error())
}
w, err := c.Data()
if err != nil {
return errors.New("fail to get smtp data writer, Error:" + err.Error())
}
_, err = w.Write([]byte(msg))
if err != nil {
return errors.New("fail to write data, Error:" + err.Error())
}
err = w.Close()
if err != nil {
return errors.New("fail to close smtp data writer, Error:" + err.Error())
}
c.Quit()
return nil
}
+13
View File
@@ -7,6 +7,8 @@ import (
"megrez/services/database" "megrez/services/database"
) )
const imagesKey = "images"
func systemInit() (err error) { func systemInit() (err error) {
l.SetFunction("systemInit") l.SetFunction("systemInit")
@@ -22,6 +24,7 @@ func systemInit() (err error) {
Username: "admin", Username: "admin",
Email: "admin@gpuManager.com", Email: "admin@gpuManager.com",
Role: 3, Role: 3,
Verify: true,
} }
result := database.DB.Create(&user) result := database.DB.Create(&user)
if result.Error != nil { if result.Error != nil {
@@ -35,6 +38,16 @@ func systemInit() (err error) {
return return
} }
st := models.System{
Key: imagesKey,
Value: "{}",
}
result = database.DB.Create(&st)
if result.Error != nil {
l.Error("Create system failed, Error: %v", result.Error)
return
}
l.Info("System init success") l.Info("System init success")
return return