Browse Source

feat: 增加LDAP接入,用户同步,密码修改登功能

tuonian 4 months ago
parent
commit
7fd52400d9
56 changed files with 1754 additions and 427 deletions
  1. 2 2
      desktop/api.go
  2. 2 2
      desktop/nginx.go
  3. 57 5
      frontend/src/api/ldap.ts
  4. 2 0
      frontend/src/api/user.ts
  5. 2 2
      frontend/src/components/BackButton.tsx
  6. 119 0
      frontend/src/components/curd/Form.tsx
  7. 28 0
      frontend/src/components/curd/form.less
  8. 20 3
      frontend/src/components/curd/index.less
  9. 65 11
      frontend/src/components/curd/index.tsx
  10. 17 0
      frontend/src/components/curd/types.ts
  11. 75 0
      frontend/src/components/error/ErrorBoundary.tsx
  12. 1 0
      frontend/src/components/error/error.less
  13. 5 0
      frontend/src/models/api.ts
  14. 40 2
      frontend/src/pages/layout/MainLayout.tsx
  15. 10 15
      frontend/src/pages/ldap/layout.tsx
  16. 1 0
      frontend/src/pages/ldap/server/index.less
  17. 165 6
      frontend/src/pages/ldap/server/index.tsx
  18. 12 0
      frontend/src/pages/ldap/user/components/UserAttributes.tsx
  19. 31 4
      frontend/src/pages/ldap/user/index.less
  20. 21 33
      frontend/src/pages/ldap/user/index.tsx
  21. 1 0
      frontend/src/pages/ldap/user/list/index.less
  22. 144 1
      frontend/src/pages/ldap/user/list/index.tsx
  23. 75 26
      frontend/src/pages/login/index.tsx
  24. 7 0
      frontend/src/pages/user/components/logout/index.tsx
  25. 73 0
      frontend/src/pages/user/components/password/index.tsx
  26. 17 18
      frontend/src/routes/index.tsx
  27. 30 17
      server/base/controller.go
  28. 3 0
      server/config/constants.go
  29. 0 50
      server/controllers/user.go
  30. 2 0
      server/db/db.go
  31. 6 6
      server/middleware/auth.go
  32. 2 2
      server/models/ldap.go
  33. 1 0
      server/models/user.go
  34. 57 28
      server/modules/ldap/client.go
  35. 13 3
      server/modules/ldap/client_test.go
  36. 58 0
      server/modules/ldap/pool.go
  37. 1 0
      server/modules/ldap/router.go
  38. 45 6
      server/modules/ldap/server_controller.go
  39. 132 8
      server/modules/ldap/service.go
  40. 7 18
      server/modules/ldap/user_controller.go
  41. 23 3
      server/modules/ldap/vo.go
  42. 6 1
      server/modules/nginx/nginx_controller/base.go
  43. 1 1
      server/modules/nginx/nginx_controller/certificate.go
  44. 1 1
      server/modules/nginx/nginx_controller/config.go
  45. 1 1
      server/modules/nginx/nginx_controller/file.go
  46. 1 1
      server/modules/nginx/nginx_controller/logger.go
  47. 5 6
      server/modules/nginx/nginx_controller/nginx.go
  48. 1 1
      server/modules/nginx/nginx_controller/server.go
  49. 5 6
      server/modules/nginx/nginx_service/nginx.go
  50. 8 7
      server/modules/oauth2/controller.go
  51. 109 0
      server/modules/user/controller.go
  52. 150 0
      server/modules/user/service.go
  53. 25 20
      server/routers/router.go
  54. 0 57
      server/service/user.go
  55. 61 54
      server/utils/file.go
  56. 8 0
      server/vo/user.go

+ 2 - 2
desktop/api.go

@@ -5,12 +5,12 @@ import (
 	"encoding/json"
 	"github.com/astaxie/beego/logs"
 	"nginx-ui/server/models"
-	"nginx-ui/server/service"
+	"nginx-ui/server/modules/user"
 )
 
 var ApiSession = Session{}
 
-var userService = service.NewUserService()
+var userService = user.NewUserService()
 var nginxApi = NewNginxApi()
 
 // Api struct

+ 2 - 2
desktop/nginx.go

@@ -4,14 +4,14 @@ import (
 	"context"
 	"github.com/astaxie/beego/logs"
 	"nginx-ui/server/models"
+	"nginx-ui/server/modules/nginx/nginx_service"
 	"nginx-ui/server/routers"
-	"nginx-ui/server/service"
 	"strings"
 )
 
 var logger = logs.GetLogger("NginxApi")
 
-var nginxService = service.NginxService{}
+var nginxService = nginx_service.NginxService{}
 
 // NginxApi struct
 type NginxApi struct {

+ 57 - 5
frontend/src/api/ldap.ts

@@ -1,6 +1,7 @@
 import {LoginReq} from "./user.ts";
 import request from "./request.ts";
-import {PageResp} from "../models/api.ts";
+import {BaseResp, PageReq, PageResp} from "../models/api.ts";
+import {User} from "../models/user.ts";
 
 
 // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -10,9 +11,31 @@ export namespace LDAP {
         id: number
         key: string
         url: string
-        admin: string
+        userName: string
         baseDN: string
         filter?: string
+        active?: boolean
+    }
+
+    export type User = {
+        id: number
+        uid: number
+        account: string
+        userName: string
+        mail: string
+        dn: string
+        attributes?: string
+        signType: string
+        serverKey: string
+        remark?: string
+        password?: string
+    }
+
+    export type VerifyData = {
+        id: number
+        filter?: string
+        active?: boolean
+        username?: string
     }
 
 }
@@ -23,21 +46,50 @@ export namespace LDAP {
 export const LDAPApis = {
 
     login: (data: LoginReq & { serverKey: string }) => {
-        return request.post('/ldap/login', data)
+        return request.post<BaseResp<User>>('/ldap/login', data)
     },
     /**
      * 加锁,避免多次访问,多次弹窗
      */
     getServer: (id: any) => {
-       return request.get(`/ldap/server/detail`, { params: { id }})
+       return request.get<BaseResp<LDAP.Server>>(`/ldap/server/detail`, { params: { id }})
+           .then(res=>{
+               if (!res.data?.data){
+                   return Promise.reject(new Error('LDAP服务不存在!'))
+               }
+               return res.data.data
+           })
     },
     getServerList: (query: any) => {
         return request.post<PageResp<LDAP.Server>>(`/ldap/server/list`, query)
     },
+    /**
+     * 保存服务
+     * @param data
+     */
+    saveServer: (data: any) => {
+        return request.post<BaseResp<LDAP.Server>>(`/ldap/server`, data)
+    },
+    verifyServer: (data: LDAP.VerifyData) => {
+        return request.post<BaseResp<LDAP.User[]>>(`/ldap/server/verify`, data)
+            .then(res=>res.data)
+    },
     /**
      * 获取当前激活的server
      */
     getActiveServer: () => {
-        return request.get(`/ldap/server/active`)
+        return request.get<BaseResp<{  server?: string }>>(`/ldap/server/active`, { disableErrorMsg: true } as any)
+    },
+    getUsers: (req: PageReq & { serverKey: string}) => {
+        return request.post<PageResp<LDAP.User>>('/ldap/user/list', req)
+    },
+    getUserDetail: (id: number) =>{
+        return request.post<BaseResp<LDAP.User>>('/ldap/user/detail', { id: id })
+    },
+    saveUser: (user: Partial<LDAP.User>) => {
+        return request.post<BaseResp<LDAP.User>>(`/ldap/user/save`, user)
+    },
+    syncUsers: (search: any) => {
+        return request.post<BaseResp<{ count: number}>>('/ldap/user/sync', search)
     }
 }

+ 2 - 0
frontend/src/api/user.ts

@@ -1,4 +1,5 @@
 import request, {getLockRequest, lockRequest, unlockRequest} from "./request.ts";
+import {BaseResp} from "../models/api.ts";
 
 export type LoginReq = {
     account: string
@@ -39,6 +40,7 @@ export const LoginApis = {
         }
         return promise;
     },
+    modifyPassword: (data: {  oldPassword: string,newPassword: string }) => request.post<BaseResp>('/user/modifyPassword', data),
     oauth2Url: ()=> request.get('/oauth2'),
     oauth2Callback: (data: SSOReq) => request.post('/oauth2/callback', data, { disableErrorMsg: true } as never)
 

+ 2 - 2
frontend/src/components/BackButton.tsx

@@ -22,8 +22,8 @@ export const BackButton = ({to}: IProps) => {
     }
   }
 
-  return (<Button
-    ghost style={{color: '#1890ff',marginRight: 10}}
+  return (<Button type="text"
+                  style={{marginRight: 10, color: '#3f94e4'}}
                   onClick={goBack}
                   icon={<ArrowLeftOutlined />} /> )
 }

+ 119 - 0
frontend/src/components/curd/Form.tsx

@@ -0,0 +1,119 @@
+import {Button, Drawer} from "antd";
+import {ForwardedRef, forwardRef, useImperativeHandle, useMemo, useRef, useState} from "react";
+import {AutoForm, AutoFormInstance, isNullOrTrue, Message} from "auto-antd";
+import {CurdColumn} from "./index.tsx";
+import {FormColumnType} from "auto-antd/dist/esm/Model";
+import {LoadingOutlined} from "@ant-design/icons";
+import {ICurdConfig, ICurdData} from "./types.ts";
+import './form.less'
+
+
+export type IProps<D extends ICurdData> = {
+    getDetail?: (data: Partial<D>) => Promise<D>
+    columns: CurdColumn[]
+    config?: ICurdConfig<D>
+    onSuccess?: (data: Partial<D>) => void
+    onSave?: (data: Partial<D>) => Promise<Partial<D>>
+}
+
+export type ICurdForm<D extends ICurdData> = {
+    show: (data: Partial<D>) => void
+}
+
+const Form = <D extends ICurdData,>({config,...props}: IProps<D>,ref: ForwardedRef<ICurdForm<D>>) => {
+
+    const [visible, setVisible] = useState<boolean>(false);
+    const [loading,setLoading] = useState<boolean>(false);
+    const [editModel,setEditModel] = useState<Partial<D>>()
+
+    const formRef = useRef<AutoFormInstance>()
+
+    const onClose = () => {
+        setVisible(false)
+    }
+
+    const formColumns = useMemo(()=>{
+        return props.columns.filter(item=>isNullOrTrue(item.editable)).map(item=>{
+            return {
+                ...item,
+                width: undefined,
+            }
+        }) as FormColumnType[]
+    },[props.columns])
+
+    const show = (data: Partial<D>) => {
+        setVisible(true)
+        if (props.getDetail){
+            setLoading(true)
+            props.getDetail?.(data)
+                .then(res=>{
+                    setEditModel(res)
+                    formRef.current?.setData(res)
+                })
+                .catch(err=>{
+                    Message.warning(err.message)
+                    setVisible(false)
+                })
+                .finally(()=>{
+                    setLoading(false)
+                })
+        }else {
+            formRef.current?.setData(data)
+        }
+    }
+
+    useImperativeHandle(ref, () => {
+        return {
+            show: show
+        }
+    })
+
+    const onSubmit = () => {
+        formRef.current?.onSyncSubmit(true)
+            .then(res=>{
+                if (!props.onSave){
+                    return Promise.reject(new Error("cancel"))
+                }
+                return props.onSave(res)
+            })
+            .then((res)=>{
+                setLoading(false)
+                props.onSuccess?.(res)
+                setVisible(false)
+            })
+            .catch(e=>{
+                console.log('submit fail',e)
+            })
+            .finally(()=>{
+                setLoading(false)
+            })
+    }
+
+
+    return (<>
+        <Drawer open={visible} destroyOnClose width={config?.editDialogWidth ?? 650}
+                title={editModel?.id ? '编辑' : '新增'}
+                height={400} onClose={onClose}>
+            <div className="form-container">
+                <div className="form-container-loading">
+                    {
+                        loading ? (<LoadingOutlined/>) : null
+                    }
+                </div>
+                <AutoForm columns={formColumns} data={editModel}
+                          formProps={{
+                              labelCol: {
+                                  span: config?.labelSpan ?? 6
+                              }
+                          }}
+                          ref={formRef as any} />
+                <div className="form-container-footer">
+                    <Button onClick={onSubmit} loading={loading} type="primary">保存</Button>
+                    <Button onClick={onClose} danger>取消</Button>
+                </div>
+            </div>
+        </Drawer>
+        </>)
+}
+
+export const CurdForm = forwardRef(Form)

+ 28 - 0
frontend/src/components/curd/form.less

@@ -0,0 +1,28 @@
+.form-container{
+  .auto-form{
+    textarea.ant-input{
+      min-width: unset;
+      width: 100%;
+    }
+    .ant-form-item-control-input-content{
+      .auto-input-wrapper{
+        width: 100%;
+        .ant-input{
+          width: 100%;
+          max-width: unset;
+        }
+        .ant-input-password{
+          max-width: unset;
+        }
+      }
+    }
+  }
+  &-loading{}
+  &-footer{
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    gap: 16px;
+  }
+}

+ 20 - 3
frontend/src/components/curd/index.less

@@ -1,10 +1,27 @@
 .curd{
+  box-sizing: border-box;
+  padding: 8px 16px;
+  &-header{
+    box-sizing: border-box;
+    padding: 8px 0;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    gap: 10px;
+  }
 
-  &-header{}
+  &-body{
 
-  &-body{}
+    .operation-list{
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      gap: 4px;
+    }
+  }
 
   &-footer{
-
+    box-sizing: border-box;
+    padding: 8px 0;
   }
 }

+ 65 - 11
frontend/src/components/curd/index.tsx

@@ -1,7 +1,12 @@
-import {AutoColumn, Message} from "auto-antd";
-import {Pagination, Table} from "antd";
-import {useEffect, useMemo, useState} from "react";
+import {AutoColumn, isNullOrTrue, Message} from "auto-antd";
+import {Button, Table} from "antd";
+import React, {useEffect, useMemo, useRef, useState} from "react";
 import {ColumnsType} from "antd/lib/table";
+import {CurdForm, ICurdForm, IProps as IFormProps} from "./Form.tsx";
+import {LDAP} from "../../api/ldap.ts";
+import {EditOutlined, PlusOutlined, SyncOutlined} from "@ant-design/icons";
+import './index.less'
+import {ICurdConfig, ICurdData} from "./types.ts";
 
 
 export type CurdColumn = AutoColumn & {
@@ -10,37 +15,65 @@ export type CurdColumn = AutoColumn & {
     hidden?: boolean
 }
 
-type PageResp<T> = { total: number, list: T[], current: number, pageSize: number }
 
-type IProps<T> = {
+type PageResp<T extends ICurdData> = { total: number, list: T[], current: number, pageSize: number }
+
+type IProps<T extends ICurdData> = IFormProps<T> & {
     columns: CurdColumn[],
     getList: (query: any) => Promise<PageResp<T> | undefined>,
+    getDetail: (data: Partial<T>) => Promise<T>
+    config?: ICurdConfig<T>
+    onSuccess?: (data: Partial<T>) => void
+    operationRender?: React.ReactNode
 }
 
 /**
  * 一个完成的增删查改界面
  * @constructor
  */
-export function CurdPage<T>({columns, getList}: IProps<T>) {
+export function CurdPage<T extends ICurdData>({columns, getList, config, operationRender, onSuccess,...props}: IProps<T>) {
 
     const [list, setList] = useState<T[]>([]);
     const [total, setTotal] = useState<number>(0)
     const [query, setQuery] = useState<any>({current: 1, pageSize: 10});
+    const [loading,setLoading] = useState<boolean>(false);
+
+    const formRef = useRef<ICurdForm<LDAP.Server>>()
+
 
     const tableColumns = useMemo(() => {
-        return columns.filter(item => !item.hidden)
+        const cols = columns.filter(item => !item.hidden)
             .map((column: CurdColumn) => {
                 return {
+                    ...column,
                     dataIndex: column.key,
                     title: column.title,
                     width: column.width,
                 }
             }) as ColumnsType<T>
+        cols.push({
+            dataIndex: "",
+            title: "操作",
+            width: 180,
+            render: (_, record, index) => {
+                return (<div className="operation-list">
+                    {
+                        isNullOrTrue(config?.editable) ?
+                            (<Button size="small" type="primary" icon={<EditOutlined />}
+                                     onClick={() => formRef.current?.show({...record})} />) : null
+                    }
+                    {config?.operationRender?.(record,index)}
+                    </div>)
+            }
+        })
+
+        return cols;
 
     }, [columns]);
 
 
     useEffect(() => {
+        setLoading(true)
         getList(query).then(res => {
             if (res){
                 setList(res.list)
@@ -48,6 +81,8 @@ export function CurdPage<T>({columns, getList}: IProps<T>) {
             }else {
                 Message.warning('无数据!')
             }
+        }).finally(()=>{
+            setLoading(false)
         })
     }, [getList, query]);
 
@@ -55,16 +90,35 @@ export function CurdPage<T>({columns, getList}: IProps<T>) {
         setQuery((q: any) => ({...q, current, pageSize}))
     }
 
+    const onSaveSuccess = (data: Partial<T>) => {
+        setQuery({...query})
+        onSuccess?.(data)
+    }
+
     return <div className="curd">
-        <div className="curd-header"></div>
+        <div className="curd-header">
+            <Button onClick={() => formRef.current?.show({})} icon={<PlusOutlined />} />
+            {operationRender}
+            <div style={{flex: 1}} />
+            <Button loading={loading} onClick={() =>setQuery({...query})} icon={<SyncOutlined />} />
+        </div>
         <div className="curd-body">
-            <Table columns={tableColumns as any} dataSource={list as any}/>
-            <Pagination total={total} pageSize={query.pageSize} current={query.current} onChange={onPageChange}/>
+            <Table columns={tableColumns as any} dataSource={list as any} pagination={{
+                total: total,
+                pageSize: query.pageSize,
+                current: query.current,
+                onChange: onPageChange,
+            }}/>
         </div>
         <div className="curd-footer">
 
         </div>
-
+        <CurdForm ref={formRef as any}
+                  columns={columns}
+                  getDetail={props.getDetail as any}
+                  onSave={props.onSave as any}
+                  onSuccess={onSaveSuccess as any}
+                  config={config as any}/>
     </div>
 
 }

+ 17 - 0
frontend/src/components/curd/types.ts

@@ -0,0 +1,17 @@
+import type * as React from "react";
+
+
+export type ICurdData = {
+    id?: any
+    [key: string]: any
+}
+
+export type ICurdConfig<T extends ICurdData> = {
+    editDialogWidth?: number
+    labelSpan?: number // Label占据的比例
+    editable?: boolean // 展示编辑按钮
+    details?: boolean // 展示详情按钮
+    operationRender?: (record: T, index: number) => React.ReactNode;
+}
+
+

+ 75 - 0
frontend/src/components/error/ErrorBoundary.tsx

@@ -0,0 +1,75 @@
+
+import './error.less'
+import {useNavigate, useRouteError} from "react-router";
+import {Button, Result, Typography} from "antd";
+import {CloseCircleOutlined} from "@ant-design/icons";
+import {useEffect, useMemo, useRef, useState} from "react";
+
+const { Paragraph, Text } = Typography;
+
+
+export const ErrorBoundary = () => {
+
+    const [countdown, setCountdown] = useState(5);
+    const timer = useRef<any>(0);
+    const error = useRouteError()
+    const navigate = useNavigate()
+
+    const onBack = () => {
+        clearTimeout(timer.current)
+        navigate(-1)
+    }
+
+    const errMsg = useMemo(()=>{
+        if (typeof error === "string"){
+            return error as string
+        }
+        if (error instanceof Error){
+            return error.message || '服务异常'
+        }
+        return JSON.stringify(error, null, 2)
+    },[error])
+
+    useEffect(()=>{
+        clearTimeout(timer.current)
+        timer.current = setTimeout(()=>{
+            if (countdown > 0){
+                setCountdown(c=>c -1)
+            }else {
+                onBack()
+            }
+        },1000)
+    },[countdown])
+
+
+    return (
+        <div className="ErrorBoundary">
+            <Result
+                status="error"
+                title="服务异常"
+                subTitle={errMsg}
+                extra={[
+                    <Button onClick={onBack} type="primary" key="console">
+                        {countdown}秒后返回上一页
+                    </Button>,
+                ]}
+            >
+                <div className="desc">
+                    <Paragraph>
+                        <Text
+                            strong
+                            style={{
+                                fontSize: 16,
+                            }}
+                        >
+                            页面错误信息如下:
+                        </Text>
+                    </Paragraph>
+                    <Paragraph>
+                        <CloseCircleOutlined className="site-result-demo-error-icon" /> {errMsg}
+                    </Paragraph>
+                </div>
+            </Result>
+        </div>
+    )
+}

+ 1 - 0
frontend/src/components/error/error.less

@@ -0,0 +1 @@
+.ErrorBoundary{}

+ 5 - 0
frontend/src/models/api.ts

@@ -7,6 +7,11 @@ export type BaseResp<T =any> = {
   data?: T
 }
 
+export type PageReq = {
+  current: number
+  pageSize: number
+}
+
 export type PageResp<T=any> = BaseResp<{
   current: number
   total: number

+ 40 - 2
frontend/src/pages/layout/MainLayout.tsx

@@ -1,10 +1,13 @@
-import {Breadcrumb, Layout, Menu} from 'antd';
+import {Breadcrumb, Dropdown, Layout, Menu, MenuProps, Space} from 'antd';
 import './layout.less'
 import {useAppDispatch, useAppSelector} from "../../store";
 import {routeActions} from "../../store/slice/route.ts";
 import {Outlet, useMatches, useNavigate} from "react-router";
-import {useEffect} from "react";
+import {useEffect, useMemo} from "react";
 import {Link} from "react-router-dom";
+import {DownOutlined} from "@ant-design/icons";
+import {ModifyPassword} from "../user/components/password";
+import {LogoutComponent} from "../user/components/logout";
 
 const BreadcrumbItem = Breadcrumb.Item
 
@@ -20,6 +23,7 @@ export const MainLayout = () => {
     const navigate = useNavigate()
 
     const matches = useMatches()
+    const user = useAppSelector(state => state.user.user)
 
     const crumbs = matches
         .filter(match => Boolean((match.handle as any)?.crumb))
@@ -34,6 +38,31 @@ export const MainLayout = () => {
         navigate(key)
     }
 
+    const personMenus = useMemo(()=>{
+        const items: MenuProps['items'] = [
+            {
+                label: (<Link to="/user/info">我的信息</Link>),
+                key: 'userinfo',
+            },
+            {
+                label: (<ModifyPassword />),
+                key: 'modifyPassword',
+            },
+            {
+                type: 'divider',
+            },
+            {
+                label: (<LogoutComponent />),
+                key: 'logout',
+            },
+        ];
+        return {
+            items: items,
+        } as MenuProps
+    },[])
+
+
+
     return (
         <Layout className="customLayout">
             <Header style={{display: 'flex', alignItems: 'center'}}>
@@ -46,6 +75,15 @@ export const MainLayout = () => {
                     style={{flex: 1, minWidth: 0}}
                     onClick={event => setNav(event.key)}
                 />
+                <div style={{flex: 1}} />
+                <Dropdown menu={personMenus}>
+                    <a onClick={e => e.preventDefault()}>
+                        <Space style={{ color: '#3f94e4'}}>
+                            { user?.nickname || '未知用户'}
+                            <DownOutlined />
+                        </Space>
+                    </a>
+                </Dropdown>
             </Header>
             <Layout>
                 {

+ 10 - 15
frontend/src/pages/ldap/layout.tsx

@@ -1,5 +1,3 @@
-import {useRoutes} from "react-router-dom";
-
 import type {RouteObject} from "react-router/dist/lib/context";
 import {Server} from "./server";
 import {UserLayout} from "./user";
@@ -7,18 +5,20 @@ import {List} from "./user/list";
 import {LDAPApis} from "../../api/ldap.ts";
 
 
-
-
-const ldapRoutes: RouteObject[] = [
+export const ldapRoutes: RouteObject[] = [
     {
-        path: "",
-        element: <Server />,
+        path: "ldap",
+        element: <Server/>,
         index: true,
     },
     {
-        path: 'server/:id',
-        Component: UserLayout,
-        loader: (params: any) => LDAPApis.getServer(params.id),
+        path: 'ldap/server/:id',
+        id: 'LDAPServerUsers',
+        element: <UserLayout/>,
+        loader: async ({params}: any) => {
+            console.log('loader ', params)
+            return LDAPApis.getServer(params.id)
+        },
         children: [
             {
                 index: true,
@@ -29,8 +29,3 @@ const ldapRoutes: RouteObject[] = [
     },
 ]
 
-export const Layout = () => {
-    return useRoutes(ldapRoutes)
-}
-
-

+ 1 - 0
frontend/src/pages/ldap/server/index.less

@@ -0,0 +1 @@
+.verify-form{}

+ 165 - 6
frontend/src/pages/ldap/server/index.tsx

@@ -1,29 +1,188 @@
 import {CurdColumn, CurdPage} from "../../../components/curd";
-import {useCallback} from "react";
-import {LDAPApis} from "../../../api/ldap.ts";
+import {useCallback, useMemo, useState} from "react";
+import {LDAP, LDAPApis} from "../../../api/ldap.ts";
+import {ICurdConfig} from "../../../components/curd/types.ts";
+import {Button, Form, Input, Modal, Switch, Table} from "antd";
+import './index.less'
+import {Message} from "auto-antd";
+import {Link} from "react-router-dom";
 
 const columns: CurdColumn[] = [
     {
         key: 'id',
         title: 'ID',
         type: 'string',
+        disabled: true,
+        required: false,
+        width: 50
+    },
+    {
+        key: 'key',
+        title: '服务编码',
+        type: 'string',
+        disabled: true,
+        placeholder: '服务唯一编码,自动生成',
+        required: false,
+        width: 120
+    },
+    {
+        key: 'url',
+        title: '服务Url',
+        type: 'string',
+        placeholder: 'eg. ldap://192.168.1.1:389',
+        width: 150
+    },
+    {
+        key: 'userName',
+        title: '管理员账号',
+        type: 'string',
+        width: 150
+    },
+    {
+        key: 'password',
+        title: '管理员密码',
+        type: 'password',
+        hidden: true,
+    },
+    {
+        key: 'baseDN',
+        type: 'string',
+        title: 'baseDN',
+        placeholder: 'eg. ou=users,dc=xxxx,dc=cn'
+    },
+    {
+        key: 'filter',
+        title: '过滤配置',
+        type: 'string',
+        placeholder: '默认:(objectClass=*)',
+        required: false,
+        width: 180
+    },
+    {
+        key: 'remark',
+        title: '备注',
+        type: 'textarea',
+        required: false,
     }
 ]
 
+const serverConfig: ICurdConfig<LDAP.Server> = {
+    editDialogWidth: 500,
+    labelSpan: 6,
+}
+
+type VerifyData = Partial<LDAP.Server> & {
+    search?: string
+}
 
 export const Server = () => {
 
-    const getList = useCallback((query: any)=>{
-        return LDAPApis.getServerList(query).then(res=>{
+    const [verifyData, setVerifyData] = useState<VerifyData>()
+    const [loading, setLoading] = useState(false)
+
+    const [searchUsers, setSearchUsers] = useState<LDAP.User[]>([])
+
+    const getList = useCallback((query: any) => {
+        return LDAPApis.getServerList(query).then(res => {
             console.log('server', res)
             return res.data.data
         })
 
-    },[])
+    }, [])
+
+    const getDetail = (data: Partial<LDAP.Server>) => {
+        if (!data.id) {
+            return Promise.resolve({} as LDAP.Server)
+        }
+        return LDAPApis.getServer(data.id).then(res => {
+            if (res) {
+                return res;
+            }
+            throw new Error('该服务不存在!')
+        })
+    }
+
+    const onSaveServer = (data: Partial<LDAP.Server>) => {
+        return LDAPApis.saveServer(data)
+            .then(res => {
+                return res.data.data as LDAP.Server;
+            })
+    }
+
+    const operationRender = (record: LDAP.Server, _: number) => {
 
+        return (<>
+            <Button onClick={() => setVerifyData(record)} size="small">验证</Button>
+            <Button size="small">
+                <Link to={`server/${record.id}/user`}>用户</Link>
+            </Button>
+        </>)
+    }
+
+    const config = useMemo(() => {
+
+        return {
+            ...serverConfig,
+            operationRender
+        } as ICurdConfig<LDAP.Server>
+
+    }, [])
+
+    const onChange = (data: Partial<VerifyData>) => {
+        setVerifyData(verify => ({...verify, ...data}))
+    }
+
+    const onVerify = () => {
+        if (!verifyData?.id) {
+            return
+        }
+        setLoading(true)
+        LDAPApis.verifyServer({id: verifyData.id, active: verifyData.active, username: verifyData.search})
+            .then(res => {
+                setLoading(false)
+                setSearchUsers(res.data || [])
+                Message.success('操作成功!')
+            })
+            .finally(()=>{
+                setLoading(false)
+            })
+    }
 
 
     return (<>
-        <CurdPage columns={columns} getList={getList} />
+        <CurdPage columns={columns} getList={getList} getDetail={getDetail}
+                  onSave={onSaveServer}
+                  onSuccess={data => setVerifyData(data)}
+                  config={config}/>
+        <Modal open={!!verifyData} title="LDAP服务验证" onCancel={() => setVerifyData(undefined)}
+               confirmLoading={loading} width={600}
+               onOk={onVerify}>
+            <Form className="verify-form">
+                <Form.Item name="id" label="URL">
+                    <span>{verifyData?.url}</span>
+                </Form.Item>
+                <Form.Item name="id" label="UserName">
+                    <span>{verifyData?.userName}</span>
+                </Form.Item>
+                <Form.Item name="baseDN" label="baseDN">
+                    <span>{verifyData?.baseDN}</span>
+                </Form.Item>
+                <Form.Item name="active" label="设置为活动服务">
+                    <Switch checked={verifyData?.active} onChange={ev => onChange({active: ev})}></Switch>
+                </Form.Item>
+                <Form.Item name="username" label="搜索用户名">
+                    <Input value={verifyData?.search} onChange={ev => onChange({search: ev.target.value})}/>
+                </Form.Item>
+                <Form.Item label="搜索结果">
+                    <div className="search-results">
+                        <Table dataSource={searchUsers} pagination={false}>
+                            <Table.Column dataIndex="account" title="uid" />
+                            <Table.Column dataIndex="userName" title="姓名" />
+                            <Table.Column dataIndex="dn" title="DN" />
+                        </Table>
+                    </div>
+                </Form.Item>
+            </Form>
+        </Modal>
     </>)
 }

+ 12 - 0
frontend/src/pages/ldap/user/components/UserAttributes.tsx

@@ -0,0 +1,12 @@
+import {LDAP} from "../../../../api/ldap.ts";
+
+
+type IProps = {
+    user: LDAP.User
+}
+
+export const UserAttributes = ({}: IProps) => {
+    return <>
+        <span>查看详情</span>
+    </>
+}

+ 31 - 4
frontend/src/pages/ldap/user/index.less

@@ -1,10 +1,37 @@
 .ldap-container{
-
+  height: 100%;
+  width: 100%;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
   &-header{
-
+    border-bottom: solid 1px #ddd;
+    display: flex;
+    align-items: center;
+    box-sizing: border-box;
+    padding: 8px 16px;
   }
 
-  &-content{}
+  &-content{
+    display: flex;
+    flex-direction: row;
+    flex: 1;
+    padding: 0 0 10px;
+    box-sizing: border-box;
 
-  &-wrap{}
+    &-menu{
+      width: 220px;
+      height: 100%;
+      overflow: auto;
+      min-width: 220px;
+    }
+  }
+
+  &-wrap{
+    flex: 1;
+    box-sizing: border-box;
+    background: white;
+    margin: 5px 10px 0 5px;
+  }
 }

+ 21 - 33
frontend/src/pages/ldap/user/index.tsx

@@ -1,28 +1,24 @@
-import {Outlet, useLoaderData, useNavigate} from "react-router";
-import {useAppDispatch} from "../../../store";
+import {Outlet, useMatches, useNavigate, useRouteLoaderData} from "react-router";
 import {useEffect, useMemo, useState} from "react";
 import {Menu, MenuProps} from "antd";
 import {BackButton} from "../../../components/BackButton.tsx";
 import {LDAP} from "../../../api/ldap.ts";
 import './index.less'
 
-
 export const UserLayout = () => {
 
-    const server = useLoaderData() as LDAP.Server;
+    const server = useRouteLoaderData("LDAPServerUsers") as LDAP.Server;
     const navigate = useNavigate()
 
-
-    const [activeKey,setActiveKey] = useState<string>('settings')
-    const [openKeys,setOpenKeys] = useState<string[]>([])
-
-    const dispatch = useAppDispatch();
+    const [activeKey,setActiveKey] = useState<string>('user')
+    const matches = useMatches()
 
 
     useEffect(()=>{
-        setActiveKey(location.pathname)
-        console.log('location changed ', location)
-    },[location.pathname])
+        const match = matches[0]
+        const key = match.pathname.substring(match.pathname.lastIndexOf('/') + 1)
+        setActiveKey(key)
+    },[matches])
 
 
     const onClick: MenuProps['onClick'] = (e) => {
@@ -30,50 +26,42 @@ export const UserLayout = () => {
         navigate(e.key, {
             replace: true,
         })
-
-        console.log('click ', e);
     };
 
-    /**
-     * 仅允许一个打开
-     * @param e
-     */
-    const onOpenChange: MenuProps['onOpenChange'] = (e)=>{
-        console.log('onOpenChange', e)
-        setOpenKeys(e)
-    }
+
+    useEffect(() => {
+        console.log('location change', activeKey)
+    }, [activeKey]);
 
     const menuItems: MenuProps['items'] = useMemo(()=>{
         return [
             {
-                key: ``,
-                title: 'Settings',
+                key: `user`,
+                label: '用户管理',
             }
         ] as MenuProps['items']
-    },[server])
+    },[])
 
     return (<div className="ldap-container">
         <div className="ldap-container-header">
             <BackButton />
-            <div>LDAP:{server.url}</div>
+            <div>LDAP:{server?.url}</div>
             <div style={{flex: 1}} />
-            <a target="_blank" style={{fontSize: 14,marginLeft: 10}} href="https://nginx.org/en/docs/">参考文档</a>
         </div>
         <div className="ldap-container-content">
             <Menu
+                className="ldap-container-content-menu"
                 onClick={onClick}
-                style={{ width: 300 }}
-                mode="inline"
                 items={menuItems}
-                activeKey={activeKey}
                 selectedKeys={[activeKey]}
-                onOpenChange={onOpenChange}
-                openKeys={openKeys}
+                activeKey={activeKey}
+                mode="inline"
+                theme="light"
+                style={{width: 220}}
             />
             <div className="ldap-container-wrap">
                 <Outlet />
             </div>
         </div>
-
     </div>)
 }

+ 1 - 0
frontend/src/pages/ldap/user/list/index.less

@@ -0,0 +1 @@
+.verify-form{}

+ 144 - 1
frontend/src/pages/ldap/user/list/index.tsx

@@ -1,7 +1,150 @@
+import {CurdColumn, CurdPage} from "../../../../components/curd";
+import {useCallback, useMemo, useState} from "react";
+import {LDAP, LDAPApis} from "../../../../api/ldap.ts";
+import {ICurdConfig} from "../../../../components/curd/types.ts";
+import {Alert, Button} from "antd";
+import './index.less'
+import {Link} from "react-router-dom";
+import {useRouteLoaderData} from "react-router";
+import {SyncOutlined} from "@ant-design/icons";
+import {Message} from "auto-antd";
+import {UserAttributes} from "../components/UserAttributes.tsx";
 
+const columns: CurdColumn[] = [
+    {
+        key: 'id',
+        title: 'ID',
+        type: 'string',
+        disabled: true,
+        required: false,
+        width: 50
+    },
+    {
+        key: 'account',
+        title: '账号(uid)',
+        type: 'string',
+        disabled: true,
+        placeholder: '服务唯一编码,自动生成',
+        required: false,
+        width: 120
+    },
+    {
+        key: 'dn',
+        title: 'DN',
+        type: 'string',
+        placeholder: 'eg. cn=Abc,dc=tonyandmoney,dc=cn',
+        width: 150
+    },
+    {
+        key: 'mail',
+        title: '邮箱',
+        type: 'string',
+        width: 150
+    },
+    {
+        key: 'attributes',
+        title: '详细属性',
+        type: 'string',
+        render: (_: any, record: LDAP.User) => <UserAttributes user={record} />
+    },
+    {
+        key: 'remark',
+        title: '备注',
+        type: 'textarea',
+        required: false,
+    }
+]
 
+const serverConfig: ICurdConfig<LDAP.User> = {
+    editDialogWidth: 500,
+    labelSpan: 6,
+}
+
+/**
+ * 用户列表的操作
+ * @constructor
+ */
 export const List = () => {
-    return (<>
 
+    const server = useRouteLoaderData("LDAPServerUsers") as LDAP.Server
+    const [loading, setLoading] = useState(false)
+    const [success,setSuccess] = useState('')
+
+    const getList = useCallback((query: any) => {
+        console.log('server...', server)
+
+        return LDAPApis.getUsers({
+            ...query,
+            serverKey: server.key,
+        }).then(res => {
+            console.log('server', res)
+            return res.data.data
+        })
+
+    }, [server])
+
+    const getDetail = (data: Partial<LDAP.User>) => {
+        if (!data.id) {
+            return Promise.resolve({} as LDAP.User)
+        }
+        return LDAPApis.getUserDetail(data.id).then(res => {
+            if (res.data?.data) {
+                return res.data.data;
+            }
+            throw new Error('该服务不存在!')
+        })
+    }
+
+    const onSaveUser = (data: Partial<LDAP.User>) => {
+        return LDAPApis.saveUser(data)
+            .then(res => {
+                return res.data.data as LDAP.User;
+            })
+    }
+
+    const syncUsers = () => {
+        setLoading(true)
+        LDAPApis.syncUsers({serverKey: server.key})
+            .then(res => {
+                console.log('sync users', res)
+                if (res.data?.data) {
+                    setSuccess(`操作成功:同步:${res.data.data.count}条数据!'`)
+                } else {
+                    Message.warning(res.data.msg || '同步异常!')
+                }
+            }).finally(() => {
+            setLoading(false)
+        })
+    }
+
+    const operationRender = (record: LDAP.User, _: number) => {
+
+        return (<>
+            <Button size="small">
+                <Link to={`server/${record.id}/user`}>重置密码</Link>
+            </Button>
         </>)
+    }
+
+    const config = useMemo(() => {
+
+        return {
+            ...serverConfig,
+            operationRender
+        } as ICurdConfig<LDAP.User>
+
+    }, [])
+
+
+    return (<>
+        {
+            success ? (<Alert type="success" message={success} style={{margin: 5}} closable={true} onClose={()=>setSuccess('')}/> ) : null
+        }
+        <CurdPage columns={columns} getList={getList} getDetail={getDetail}
+                  operationRender={<>
+                      <Button type="primary" icon={<SyncOutlined/>} onClick={syncUsers} loading={loading}>同步</Button>
+                  </>}
+                  onSave={onSaveUser}
+                  config={config}/>
+    </>)
 }

+ 75 - 26
frontend/src/pages/login/index.tsx

@@ -4,41 +4,67 @@ import {Button, Form, Input, Spin, Tabs} from "antd";
 import {TabsProps} from "antd/lib/tabs";
 import {Link} from "react-router-dom";
 import {LoginApis, LoginReq } from "../../api/user.ts";
-import { useState} from "react";
+import {useEffect, useMemo, useState} from "react";
 import {useAppDispatch} from "../../store";
 import {UserActions} from "../../store/slice/user.ts";
 import {Message} from "auto-antd";
 import {useNavigate } from "react-router";
 import {cacheTo, parseQuery, useQuery} from "../../utils";
+import {LDAPApis} from "../../api/ldap.ts";
+import {BaseResp} from "../../models/api.ts";
+import {User} from "../../models/user.ts";
 
-const AccountPanel = ()=>{
+type IProps = {
+    ldap?: string
+}
+
+const AccountPanel = ({ ldap }: IProps)=>{
 
     const [loading,setLoading] = useState(false)
     const dispatch = useAppDispatch()
     const navigate = useNavigate()
     const query = useQuery<{to?: string}>()
 
-    const onSubmit = (values: LoginReq)=>{
-        console.log('submit',values);
+    const onLoginRes = (resp: BaseResp<User>) => {
+        if (resp.data){
+            dispatch(UserActions.setUser(resp.data));
+            navigate(query?.to || '/')
+        }
+        if (resp.code == 0){
+            Message.success(resp.msg)
+        }else {
+            Message.warning(resp.msg)
+        }
+    }
+
+    const onPasswordLogin = (values: LoginReq) => {
         setLoading(true);
         LoginApis.login(values)
             .then(({data})=>{
                 console.log('login resp',data)
-                if (data.data){
-                    dispatch(UserActions.setUser(data.data));
-                    navigate(query?.to || '/')
-                }
-                if (data.code == 0){
-                    Message.success(data.msg)
-                }else {
-                    Message.warning(data.msg)
-                }
+                onLoginRes(data)
             })
             .finally(()=>{
                 setLoading(false)
             })
     }
 
+    const onSubmit = (values: LoginReq)=>{
+        console.log('submit',values);
+        if (ldap){
+            setLoading(true);
+            LDAPApis.login({ ...values, serverKey: ldap })
+                .then(({data})=>{
+                    onLoginRes(data)
+                })
+                .finally(()=>{
+                    setLoading(false)
+                })
+        }else {
+            onPasswordLogin(values)
+        }
+    }
+
     return (
         <Form onFinish={onSubmit} labelCol={{span: 4}}>
             <Form.Item name="account" label="账号" rules={[{required: true,message: '请输入账号'}]}>
@@ -60,6 +86,8 @@ export const LoginPage = ()=>{
 
     const [activeKey,setActiveKey] = useState('account')
     const [loading,setLoading] = useState(false)
+    const [ldap,setLdap] = useState('')
+
     const { query } = parseQuery()
     console.log('query', query)
 
@@ -90,22 +118,43 @@ export const LoginPage = ()=>{
         }
     }
 
-
-    const tabItems:TabsProps["items"] = [
-        {
+    useEffect(() => {
+        LDAPApis.getActiveServer().then(res=>{
+            if (res.data.data?.server){
+                setLdap(res.data.data.server)
+                setActiveKey('ldap')
+            }else {
+                setLdap('')
+            }
+        })
+    }, []);
+
+    const tabItems = useMemo(()=>{
+        const tabItems:TabsProps["items"] = []
+        if (ldap){
+            tabItems.push({
+                label: 'LDAP',
+                key: 'ldap',
+                children: <AccountPanel ldap={ldap} />
+            })
+        }
+        tabItems.push( {
             label: '账号密码',
             key: 'account',
             children: <AccountPanel />
-        },
-    ]
-    // @ts-ignore
-    if (window.CONFIG.SSO){
-        tabItems.push({
-            label: "SSO",
-            key: 'sso',
-            children: <Spin />
-        })
-    }
+        },)
+        // @ts-ignore
+        if (window.CONFIG.SSO){
+            tabItems.push({
+                label: "SSO",
+                key: 'sso',
+                children: <Spin />
+            })
+        }
+        return tabItems;
+    },[ldap])
+
+
 
     return (<div className="login-page">
         <div className="login-container">

+ 7 - 0
frontend/src/pages/user/components/logout/index.tsx

@@ -0,0 +1,7 @@
+
+export const LogoutComponent = () => {
+
+    return <>
+        <span>退出登录</span>
+    </>
+}

+ 73 - 0
frontend/src/pages/user/components/password/index.tsx

@@ -0,0 +1,73 @@
+import {useRef, useState} from "react";
+import {Form, FormInstance, Input, Modal} from "antd";
+import {Message} from "auto-antd";
+import {LoginApis} from "../../../../api/user.ts";
+
+
+export const ModifyPassword = () => {
+
+
+    const [visible,setVisible] = useState(false);
+    const [loading,setLoading] = useState(false);
+
+    const [model,setModel] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
+
+    const formRef = useRef<FormInstance>();
+
+    const handleModifyPassword = (values: any) => {
+        setLoading(true)
+        LoginApis.modifyPassword({
+            newPassword: values.newPassword,
+            oldPassword: values.oldPassword,
+        }).then(res=>{
+            if (res.data){
+                Message.success(res.data.msg)
+            }
+            setTimeout(()=>{
+                setVisible(false)
+            },1000)
+        }).finally(()=>{
+            setLoading(false)
+        })
+    }
+
+    const onSubmit = () => {
+        formRef.current?.validateFields()
+            .then(values => {
+                console.log(values);
+                if (values.newPassword != values.confirmPassword){
+                    Message.warning('两次输入的新密码不一致,请检查!')
+                    return
+                }
+                handleModifyPassword(values)
+            })
+            .catch(e=>{
+                console.log('validateFields fail',e)
+            })
+    }
+
+    const onChange = (data:any)=>{
+        setModel(m=>({...m,...data}))
+    }
+
+
+    return <>
+        <span onClick={() =>setVisible(true)}>修改密码</span>
+        <Modal title="修改密码" open={visible} onCancel={()=>setVisible(false)}
+               onOk={onSubmit}
+               confirmLoading={loading}
+               destroyOnClose={true}>
+            <Form ref={formRef as any} labelCol={{span: 6}}>
+                <Form.Item name="oldPassword" label="当前密码" rules={[{ required: true, message: '请输入当前密码'}]}>
+                    <Input.Password value={model.oldPassword} onChange={ev=>onChange({ oldPassword: ev.target.value })}/>
+                </Form.Item>
+                <Form.Item name="newPassword" label="新密码" rules={[{ required: true, message: '请输入新密码'}]}>
+                    <Input.Password value={model.newPassword} onChange={ev=>onChange({ oldPassword: ev.target.value })}/>
+                </Form.Item>
+                <Form.Item name="confirmPassword" label="再次输入新密码" rules={[{ required: true, message: '请再次输入新密码'}]}>
+                    <Input.Password value={model.confirmPassword} onChange={ev=>onChange({ oldPassword: ev.target.value })}/>
+                </Form.Item>
+            </Form>
+        </Modal>
+    </>
+}

+ 17 - 18
frontend/src/routes/index.tsx

@@ -14,7 +14,8 @@ import {ErrorPage} from "../pages/error";
 import {LoginPage} from "../pages/login";
 import {SignupPage} from "../pages/signup";
 import {NginxLayout} from "../pages/nginx/layout.tsx";
-import {Layout as LDAP } from '../pages/ldap/layout.tsx'
+import {ldapRoutes} from '../pages/ldap/layout.tsx'
+import {ErrorBoundary} from "../components/error/ErrorBoundary.tsx";
 /**
  * @author tuonian
  * @date 2023/6/26
@@ -74,6 +75,18 @@ export const RouteWrapper = ({Component, ...props}: RouteWrapperProps) => {
 
 
 const router = createHashRouter([
+    {
+        path: "/error",
+        element: <ErrorPage/>
+    },
+    {
+        path: "/login",
+        element: <LoginPage />
+    },
+    {
+        path: "/signup",
+        element: <SignupPage />
+    },
     {
         path: "/*",
         element: <RouteWrapper Component={MainLayout} />,
@@ -87,24 +100,10 @@ const router = createHashRouter([
                 //     label: 'Nginx列表'
                 // }
             },
-            {
-                path: "ldap/*",
-                Component: LDAP,
-            }
-        ]
+            ...ldapRoutes
+        ],
+        errorElement: <ErrorBoundary />
     },
-    {
-        path: "/error",
-        element: <ErrorPage/>
-    },
-    {
-        path: "/login",
-        element: <LoginPage />
-    },
-    {
-        path: "/signup",
-        element: <SignupPage />
-    }
 ])
 
 export const MyRouter = () => {

+ 30 - 17
server/controllers/default.go → server/base/controller.go

@@ -1,4 +1,4 @@
-package controllers
+package base
 
 import (
 	"encoding/json"
@@ -10,14 +10,14 @@ import (
 	"strconv"
 )
 
-type BaseController struct {
+type Controller struct {
 	beego.Controller
 	jsonData *models.RespData
 	// Json real data
 	respData map[string]any
 }
 
-func (c *BaseController) Json() {
+func (c *Controller) Json() {
 	c.checkJsonData()
 	if c.respData != nil {
 		c.jsonData.Data = c.respData
@@ -26,16 +26,16 @@ func (c *BaseController) Json() {
 	c.ServeJSON()
 }
 
-func (c *BaseController) PostJson(json interface{}) {
+func (c *Controller) PostJson(json interface{}) {
 	c.Data["json"] = json
 	c.ServeJSON()
 }
 
-func (c *BaseController) ErrorJson(error error) {
+func (c *Controller) ErrorJson(error error) {
 	c.SetCode(-1).SetMsg(error.Error()).Json()
 }
 
-func (c *BaseController) checkJsonData() *BaseController {
+func (c *Controller) checkJsonData() *Controller {
 	data := c.jsonData
 	if data == nil {
 		data = &models.RespData{
@@ -48,13 +48,13 @@ func (c *BaseController) checkJsonData() *BaseController {
 	return c
 }
 
-func (c *BaseController) SetData(v interface{}) *BaseController {
+func (c *Controller) SetData(v interface{}) *Controller {
 	c.checkJsonData()
 	c.jsonData.Data = v
 	return c
 }
 
-func (c *BaseController) AddRespData(k string, v interface{}) *BaseController {
+func (c *Controller) AddRespData(k string, v interface{}) *Controller {
 	c.checkJsonData()
 	if c.respData == nil {
 		c.respData = map[string]any{}
@@ -63,21 +63,21 @@ func (c *BaseController) AddRespData(k string, v interface{}) *BaseController {
 	return c
 }
 
-func (c *BaseController) SetCode(code int) *BaseController {
+func (c *Controller) SetCode(code int) *Controller {
 	c.checkJsonData()
 	c.jsonData.Code = code
 	return c
 }
-func (c *BaseController) SetMsg(msg string) *BaseController {
+func (c *Controller) SetMsg(msg string) *Controller {
 	c.checkJsonData()
 	c.jsonData.Msg = msg
 	return c
 }
 
-func (c *BaseController) GetParam(k string) string {
+func (c *Controller) GetParam(k string) string {
 	return c.Ctx.Input.Param(k)
 }
-func (c *BaseController) GetIntParam(k string) (int, error) {
+func (c *Controller) GetIntParam(k string) (int, error) {
 	idStr := c.Ctx.Input.Param(k)
 	id, err := strconv.Atoi(idStr)
 	logs.Info("id", id)
@@ -87,7 +87,20 @@ func (c *BaseController) GetIntParam(k string) (int, error) {
 	return id, nil
 }
 
-func (c *BaseController) GetUser(required bool) *models.User {
+func (c *Controller) GetQuery(k string) string {
+	return c.Ctx.Input.Query(k)
+}
+func (c *Controller) GetIntQuery(k string) (int, error) {
+	idStr := c.Ctx.Input.Query(k)
+	id, err := strconv.Atoi(idStr)
+	logs.Info("id", id)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}
+
+func (c *Controller) GetUser(required bool) *models.User {
 	data := c.GetSession("user")
 	if data == nil && required {
 		middleware.WriteForbidden(c.Ctx.ResponseWriter)
@@ -100,13 +113,13 @@ func (c *BaseController) GetUser(required bool) *models.User {
 	return &user
 }
 
-func (c *BaseController) RequiredUser() *models.User {
+func (c *Controller) RequiredUser() *models.User {
 	return c.GetUser(true)
 }
 
 // ReadBody data 非指针
-func (c *BaseController) ReadBody(data any) bool {
-	err := json.Unmarshal(c.Ctx.Input.RequestBody, &data)
+func (c *Controller) ReadBody(data any) bool {
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, data)
 	if err != nil {
 		logs.Error(err, string(c.Ctx.Input.RequestBody))
 		c.ErrorJson(errors.New("数据解析异常!"))
@@ -115,6 +128,6 @@ func (c *BaseController) ReadBody(data any) bool {
 	return true
 }
 
-func (c *BaseController) Forbidden() {
+func (c *Controller) Forbidden() {
 	c.SetCode(403).SetMsg("您没有权限操作该实例!").Json()
 }

+ 3 - 0
server/config/constants.go

@@ -0,0 +1,3 @@
+package config
+
+const ReplacePassword = "******"

+ 0 - 50
server/controllers/user.go

@@ -1,50 +0,0 @@
-package controllers
-
-import (
-	"encoding/json"
-	"github.com/astaxie/beego/logs"
-	"nginx-ui/server/models"
-	"nginx-ui/server/service"
-)
-
-type UserController struct {
-	BaseController
-	service *service.UserService
-}
-
-func NewUserController() *UserController {
-
-	return &UserController{
-		service: service.NewUserService(),
-	}
-}
-
-// Login 登录
-func (c *UserController) Login() {
-	var user models.User
-	err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
-	if err != nil {
-		logs.Error(err, string(c.Ctx.Input.RequestBody))
-		c.ErrorJson(err)
-		return
-	}
-	resp := c.service.Login(&user)
-	if resp.Success() {
-		c.SetSession("user", user)
-	}
-	c.PostJson(resp)
-}
-
-func (c *UserController) User() {
-	user := c.RequiredUser()
-	if user == nil {
-		return
-	}
-	c.SetData(user).Json()
-}
-
-// Register 用户注册
-func (c *UserController) Register() {
-	resp := c.service.SignUp(c.Ctx.Input.RequestBody)
-	c.PostJson(resp)
-}

+ 2 - 0
server/db/db.go

@@ -44,4 +44,6 @@ func Init() {
 
 	orm.RunSyncdb("default", false, true)
 
+	orm.Debug = true
+
 }

+ 6 - 6
server/middleware/auth.go

@@ -15,12 +15,12 @@ import (
 
 // 白名单,不需要登录即可访问
 var whitelist = map[string]bool{
-	"/user/login":      true,
-	"/user/register":   true,
-	"/oauth2":          true,
-	"/oauth2/callback": true,
-	"/ldap/login":      true,
-	"/ldap/server":     true,
+	"/user/login":         true,
+	"/user/register":      true,
+	"/oauth2":             true,
+	"/oauth2/callback":    true,
+	"/ldap/login":         true,
+	"/ldap/server/active": true,
 }
 
 var UnauthorizedResp = `{"code": 401, "msg":"未登录或者登录已过期!"}`

+ 2 - 2
server/models/ldap.go

@@ -7,7 +7,7 @@ type LdapServer struct {
 	Uid      string `orm:"size(100)" json:"uid"`
 	Name     string `orm:"size(255)" json:"name"`
 	Key      string `orm:"unique" json:"key"`
-	Url      string `json:"dn"`
+	Url      string `json:"url"`
 	Active   bool   `json:"active"`   // 是否激活可用
 	UserName string `json:"userName"` // 直接存储JSON数据,数组格式,多个
 	Password string `json:"password"`
@@ -30,5 +30,5 @@ type LdapUser struct {
 	SignType   string `json:"signType"`                         // 密码加密方式
 	Password   string `json:"password"`
 	Remark     string `json:"remark"`
-	ServerKey  string `json:"serverId"`
+	ServerKey  string `json:"serverKey"`
 }

+ 1 - 0
server/models/user.go

@@ -16,6 +16,7 @@ type User struct {
 	Roles     string    `json:"roles"`
 	Password  string    `json:"password"`
 	Remark    string    `json:"remark"`
+	Source    string    `json:"source"` // 账号来源,默认或者自有账号
 	CreatedAt time.Time `orm:"auto_now_add;type(datetime)" json:"createdAt"`
 }
 

+ 57 - 28
server/modules/ldap/client.go

@@ -1,15 +1,12 @@
 package ldap
 
 import (
-	"crypto/md5"
-	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"github.com/astaxie/beego/logs"
 	"github.com/go-ldap/ldap/v3"
 	"nginx-ui/server/models"
-	"strings"
 )
 
 type Client struct {
@@ -20,15 +17,25 @@ type Client struct {
 	Admin     string
 	Password  string
 	ServerKey string
+	pool      *ConnectionPool
 }
 
 var ActiveClients = make(map[string]*Client)
 
-func Create(url string, baseDN string) *Client {
+func createClient(url string, baseDN string, createPool bool) *Client {
 	var client = &Client{
 		Url:    url,
 		BaseDN: baseDN,
 	}
+	if createPool {
+		client.pool = NewConnectionPool(5, func() (interface{}, error) {
+			c := createClient(url, baseDN, false)
+			if c.Connected {
+				return c, nil
+			}
+			return nil, errors.New("连接服务失败!")
+		})
+	}
 	conn, err := ldap.DialURL(client.Url)
 	if err != nil {
 		logs.Error("dialUrl fail: %v", err)
@@ -43,14 +50,14 @@ func Create(url string, baseDN string) *Client {
 func GetActiveClient(server *models.LdapServer) (*Client, error) {
 	client := ActiveClients[server.Key]
 	if client == nil {
-		client = Create(server.Url, server.BaseDN)
+		client = createClient(server.Url, server.BaseDN, true)
 		err := client.Bind(server.UserName, server.Password, true)
 		if err != nil {
 			logs.Error("Bind fail: %v", err)
 			return nil, err
 		}
-		ActiveClients[server.Key] = client
 		client.ServerKey = server.Key
+		ActiveClients[server.Key] = client
 	}
 	return client, nil
 }
@@ -61,6 +68,14 @@ func (c *Client) Close() {
 	}
 }
 
+func (c *Client) Acquire() (*Client, error) {
+	inter, err := c.pool.Acquire()
+	if err != nil {
+		return nil, err
+	}
+	return inter.(*Client), err
+}
+
 // Bind 验证账号密码?
 func (c *Client) Bind(username string, password string, isAdmin bool) error {
 	err := c.Conn.Bind(username, password)
@@ -121,11 +136,35 @@ func (c *Client) Search(filter string) ([]models.LdapUser, error) {
 	return users, nil
 }
 
-func (c *Client) ModifyAdminPassword() error {
+// 通过管理员修改密码,而非自行修改密码
+func (c *Client) ModifyPasswordByAdmin(dn string, newPassword string) error {
+	passwordModifyRequest := ldap.NewPasswordModifyRequest(dn, "", newPassword)
+	_, err := c.PasswordModify(passwordModifyRequest)
+	if err != nil {
+		logs.Error("Password could not be changed: %s", err.Error())
+		return err
+	}
 	return nil
 }
 
-func (c *Client) ModifyPassword() error {
+// ModifyPassword 自行修改密码
+func (c *Client) ModifyPassword(userDN string, password string, newPassword string) error {
+	l, err := c.Acquire()
+	if err != nil {
+		logs.Error(err)
+		return err
+	}
+	err = l.Bind(userDN, password, false)
+	if err != nil {
+		logs.Error(err)
+		return errors.New("密码验证失败:" + err.Error())
+	}
+	passwordModifyRequest := ldap.NewPasswordModifyRequest("", password, newPassword)
+	_, err = l.PasswordModify(passwordModifyRequest)
+
+	if err != nil {
+		logs.Error("Password could not be changed: %s", err.Error())
+	}
 	return nil
 }
 
@@ -145,26 +184,16 @@ func (c *Client) Authentication(account string, password string) (*models.LdapUs
 		return nil, errors.New("未找到用户或者账号重复!")
 	}
 
-	userDN := users[0].Account
-	userPassword := users[0].Password
-	if strings.HasPrefix(userPassword, "{MD5}") {
-		pass := md5.Sum([]byte(password))
-		password = hex.EncodeToString(pass[:])
-		if strings.TrimPrefix(userPassword, "{MD5}") != password {
-			return nil, errors.New("登录失败,账号或者密码不正确!")
-		}
-	} else {
-		client, err := ldap.DialURL(c.Url)
-		if err != nil {
-			logs.Error("DialURL failed, err:%v", err)
-			return nil, errors.New("服务连接异常!")
-		}
-		defer client.Close()
-		err = client.Bind(userDN, password)
-		if err != nil {
-			logs.Error("GSSAPIBind failed, err:%v", err)
-			return nil, errors.New("登录失败,账号或者密码不正确!")
-		}
+	userDN := users[0].DN
+	client, err := c.Acquire()
+	if err != nil {
+		return nil, err
+	}
+	err = client.Bind(userDN, password, false)
+	if err != nil {
+		logs.Error("GSSAPIBind failed, err:%v", err)
+		return nil, errors.New("登录失败,账号或者密码不正确!")
 	}
+	c.pool.Release(client)
 	return &users[0], nil
 }

+ 13 - 3
server/modules/ldap/client_test.go

@@ -3,21 +3,31 @@ package ldap
 import (
 	"github.com/astaxie/beego/logs"
 	"log"
+	"nginx-ui/server/models"
 	"testing"
 )
 
 func TestCreate(t *testing.T) {
-	client := Create("ldap://192.168.1.95:389", "dc=tonyandmoney,dc=cn")
+	server := models.LdapServer{
+		Url:    "ldap://192.168.1.95:389",
+		BaseDN: "ou=users,dc=tonyandmoney,dc=cn",
+		Key:    "Test",
+	}
+	client, err := GetActiveClient(&server)
+	if err != nil {
+		log.Panic(err)
+	}
 	if client.Connected == false {
 		log.Panic("connect fail")
 	}
-	err := client.Bind("cn=admin,dc=tonyandmoney,dc=cn", "TQ1312@kmlsx", true)
+	err = client.Bind("cn=admin,dc=tonyandmoney,dc=cn", "TQ1312@kmlsx", true)
 	if err != nil {
 		logs.Error("Test error:", err)
 		return
 	}
 	logs.Info("Test: ok")
-	users, err := client.Search("")
+	// tuonian
+	users, err := client.Search("(&(objectClass=*)(uid=tuonian))")
 	if err != nil {
 		log.Panic(err)
 	}

+ 58 - 0
server/modules/ldap/pool.go

@@ -0,0 +1,58 @@
+package ldap
+
+import (
+	"sync"
+)
+
+type ConnectionPool struct {
+	pool    chan interface{}            // 连接缓冲池
+	factory func() (interface{}, error) // 创建新连接的工厂方法
+	size    int                         // 连接池大小
+	mu      sync.Mutex                  // 并发控制
+	closed  bool                        // 标记连接池是否已关闭
+}
+
+func NewConnectionPool(size int, factory func() (interface{}, error)) *ConnectionPool {
+	return &ConnectionPool{
+		pool:    make(chan interface{}, size),
+		factory: factory,
+		size:    size,
+	}
+}
+
+func (p *ConnectionPool) Acquire() (interface{}, error) {
+	select {
+	case conn := <-p.pool:
+		return conn, nil
+	default:
+		return p.factory()
+	}
+}
+
+func (p *ConnectionPool) Release(conn interface{}) {
+	if conn == nil {
+		return
+	}
+	select {
+	case p.pool <- conn:
+	default:
+		if closer, ok := conn.(interface{ Close() }); ok {
+			closer.Close()
+		}
+	}
+}
+
+func (p *ConnectionPool) Close() {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if !p.closed {
+		p.closed = true
+		close(p.pool)
+		for conn := range p.pool {
+			// 关闭连接
+			if closer, ok := conn.(interface{ Close() }); ok {
+				closer.Close()
+			}
+		}
+	}
+}

+ 1 - 0
server/modules/ldap/router.go

@@ -10,6 +10,7 @@ func InitRouter(prefix string) *beego.Namespace {
 		beego.NSRouter("/server/active", &ServerController{}, "get:GetServer"),
 		beego.NSRouter("/server/list", &ServerController{}, "post:GetServers"),
 		beego.NSRouter("/server", &ServerController{}, "post:Update"),
+		beego.NSRouter("/server/verify", &ServerController{}, "post:Verify"),
 		beego.NSRouter("/server/detail", &ServerController{}, "get:GetServerDetail"),
 
 		beego.NSRouter("/login", &UserController{}, "post:Login"),

+ 45 - 6
server/modules/ldap/server_controller.go

@@ -3,13 +3,14 @@ package ldap
 import (
 	"errors"
 	"github.com/astaxie/beego/orm"
-	"nginx-ui/server/controllers"
+	"nginx-ui/server/base"
+	"nginx-ui/server/config"
 	"nginx-ui/server/models"
 	"nginx-ui/server/vo"
 )
 
 type ServerController struct {
-	controllers.BaseController
+	base.Controller
 }
 
 var ServiceInstance = new(Service)
@@ -23,6 +24,7 @@ func (c *ServerController) GetServer() {
 	}
 	resp := make(map[string]interface{})
 	resp["server"] = server.Key
+	c.SetData(resp).Json()
 }
 
 // GetServerDetail 获取用户所有的LDAP连接
@@ -33,7 +35,7 @@ func (c *ServerController) GetServerDetail() {
 		return
 	}
 
-	id, err := c.GetIntParam("id")
+	id, err := c.GetIntQuery("id")
 	if err != nil {
 		c.ErrorJson(err)
 		return
@@ -48,7 +50,7 @@ func (c *ServerController) GetServerDetail() {
 		c.ErrorJson(err)
 		return
 	}
-	server.Password = ""
+	server.Password = config.ReplacePassword
 	c.SetData(server).Json()
 }
 
@@ -61,7 +63,7 @@ func (c *ServerController) GetServers() {
 	}
 
 	req := vo.PageReq{}
-	if !c.ReadBody(req) {
+	if !c.ReadBody(&req) {
 		return
 	}
 	resp, err := ServiceInstance.GetServers(current, &req)
@@ -77,7 +79,7 @@ func (c *ServerController) GetServers() {
 func (c *ServerController) Update() {
 	if current := c.RequiredUser(); current != nil {
 		var body = models.LdapServer{}
-		if !c.ReadBody(body) {
+		if !c.ReadBody(&body) {
 			return
 		}
 		user, err := ServiceInstance.Update(current, &body)
@@ -88,3 +90,40 @@ func (c *ServerController) Update() {
 		c.SetData(user).Json()
 	}
 }
+
+// Verify 保存或者修改
+// post /ldap/server/verify
+func (c *ServerController) Verify() {
+	if current := c.RequiredUser(); current != nil {
+		var body = VerifyReq{}
+		if !c.ReadBody(&body) {
+			return
+		}
+		list, err := ServiceInstance.VerifyServer(&body)
+		if err != nil {
+			c.ErrorJson(err)
+			return
+		}
+		for _, user := range list {
+			user.Password = config.ReplacePassword
+		}
+		if body.Active {
+			o := orm.NewOrm()
+			o.Begin()
+			_, err := o.Raw("UPDATE `ldap_server` SET `active` = 0 WHERE `active` = 1").Exec()
+			if err != nil {
+				o.Rollback()
+				c.ErrorJson(err)
+				return
+			}
+			_, err = o.Update(&models.LdapServer{Id: body.Id, Active: true}, "Active")
+			if err != nil {
+				o.Rollback()
+				c.ErrorJson(err)
+				return
+			}
+			err = o.Commit()
+		}
+		c.SetData(&list).Json()
+	}
+}

+ 132 - 8
server/modules/ldap/service.go

@@ -4,8 +4,10 @@ import (
 	"crypto/md5"
 	"encoding/hex"
 	"errors"
+	"fmt"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
+	"nginx-ui/server/config"
 	"nginx-ui/server/models"
 	"nginx-ui/server/vo"
 )
@@ -28,10 +30,12 @@ func (c *Service) GetServer() (*models.LdapServer, error) {
 
 func (c *Service) Login(req *LDAPLoginReq) (*models.User, error) {
 
-	server := models.LdapServer{Key: req.Server}
+	server := models.LdapServer{Key: req.ServerKey}
 	o := orm.NewOrm()
 	err := o.Read(&server, "Key")
-
+	if err != nil {
+		return nil, errors.New("未找到对应的LDAP服务!")
+	}
 	client, err := GetActiveClient(&server)
 	if err != nil {
 		return nil, err
@@ -58,10 +62,19 @@ func (c *Service) Login(req *LDAPLoginReq) (*models.User, error) {
 		user.Password = ldapUser.Password
 		user.Account = ldapUser.Account
 		user.Nickname = ldapUser.UserName
+		user.Source = "LDAP"
 		_, err := o.Insert(&user)
 		if err != nil {
 			return nil, err
 		}
+	} else if user.Source == "LDAP" {
+		// 更新用户
+		user.Password = ldapUser.Password
+		user.Nickname = ldapUser.UserName
+		_, err = o.Update(&user, "Password", "Nickname")
+		if err != nil {
+			return nil, err
+		}
 	}
 	user.Password = ""
 	return &user, nil
@@ -88,6 +101,9 @@ func (c *Service) GetServers(current *models.User, req *vo.PageReq) (*vo.PageRes
 	qs.Limit(req.PageSize)
 	var list []*models.LdapServer
 	_, err = qs.All(&list)
+	for _, v := range list {
+		v.Password = config.ReplacePassword
+	}
 
 	if err != nil {
 		return nil, err
@@ -130,6 +146,42 @@ func (c *Service) SyncUsers(current *models.User, req *LDAPUserSyncReq) (int, er
 	return len(users), nil
 }
 
+// SyncUser SyncUsers 同步用户信息
+// post /ldap/user/sync
+func (c *Service) SyncUser(server *models.LdapServer, current *models.LdapUser) error {
+	o := orm.NewOrm()
+	if server == nil {
+		server := &models.LdapServer{Key: current.ServerKey}
+		err := o.Read(server, "Key")
+		if err != nil {
+			return err
+		}
+	}
+	client, err := GetActiveClient(server)
+	if err != nil {
+		return err
+	}
+
+	users, err := client.Search(fmt.Sprintf("(&(objectClass=*)(uid=%s))", current.Account))
+	if err != nil {
+		return err
+	}
+	if len(users) != 1 {
+		return errors.New("账号不存在或者账号重复!")
+	}
+	user := users[0]
+	user.Id = current.Id
+	user.ServerKey = current.ServerKey
+	user.Uid = current.Uid
+	user.Remark = current.Remark
+
+	_, err = o.Update(&user)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // Update 保存或者修改
 // post /ldap/server
 func (c *Service) Update(current *models.User, body *models.LdapServer) (*models.LdapServer, error) {
@@ -145,7 +197,7 @@ func (c *Service) Update(current *models.User, body *models.LdapServer) (*models
 	if body.Id == 0 {
 		exist := models.LdapServer{Key: body.Key}
 		err := o.Read(&exist, "Key")
-		if err != nil {
+		if err != nil && !errors.Is(err, orm.ErrNoRows) {
 			return nil, err
 		}
 		if exist.Id > 0 {
@@ -153,12 +205,20 @@ func (c *Service) Update(current *models.User, body *models.LdapServer) (*models
 		}
 	}
 	if body.Id > 0 {
-		_, err := o.Update(&body, "Id")
+		exist := models.LdapServer{Id: body.Id}
+		err := o.Read(&exist, "Id")
+		if err != nil {
+			return nil, err
+		}
+		if config.ReplacePassword == body.Password {
+			body.Password = exist.Password
+		}
+		_, err = o.Update(body)
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		id, err := o.Insert(&body)
+		id, err := o.Insert(body)
 		if err != nil {
 			return nil, err
 		}
@@ -167,15 +227,78 @@ func (c *Service) Update(current *models.User, body *models.LdapServer) (*models
 	return body, nil
 }
 
+// VerifyServer 验证服务
+func (c *Service) VerifyServer(req *VerifyReq) ([]models.LdapUser, error) {
+	var server = &models.LdapServer{
+		Id: req.Id,
+	}
+	o := orm.NewOrm()
+	err := o.Read(server, "Id")
+	if err != nil {
+		return nil, err
+	}
+	client, err := GetActiveClient(server)
+	if err != nil {
+		return nil, err
+	}
+
+	if req.Filter == "" && req.Username != "" {
+		req.Filter = fmt.Sprintf("(&(objectClass=*)(uid=%s))", req.Username)
+	}
+	if req.Filter != "" {
+		users, err := client.Search(req.Filter)
+		if err != nil {
+			return nil, err
+		}
+		return users, nil
+	}
+	return make([]models.LdapUser, 0), nil
+}
+
 // UpdateUserPassword 更新用户密码
 // post /ldap/user/modifyPassword
-func (c *Service) UpdateUserPassword() {
-
+func (c *Service) UpdateUserPassword(req *UpdatePasswordReq, byAdmin bool) error {
+	o := orm.NewOrm()
+	user := models.LdapUser{
+		Account: req.Account,
+	}
+	err := o.Read(&user, "Account")
+	if err != nil {
+		if errors.Is(err, orm.ErrNoRows) {
+			return nil
+		}
+		logs.Error("read user fail: %v", err)
+		return err
+	}
+	server := models.LdapServer{
+		Key: user.ServerKey,
+	}
+	err = o.Read(&server, "Key")
+	if err != nil {
+		return err
+	}
+	client, err := GetActiveClient(&server)
+	if err != nil {
+		return err
+	}
+	if byAdmin {
+		err = client.ModifyPasswordByAdmin(user.DN, req.Password)
+	} else {
+		err = client.ModifyPassword(user.DN, req.OldPassword, req.Password)
+	}
+	if err != nil {
+		return err
+	}
+	err = c.SyncUser(&server, &user)
+	if err != nil {
+		return errors.New("密码更新成功,但更新用户信息失败:" + err.Error())
+	}
+	return nil
 }
 
 // GetUsers 获取全部用户
 // get /ldap/users
-func (c *Service) GetUsers(current *models.User, req *vo.PageReq) (*vo.PageResp, error) {
+func (c *Service) GetUsers(current *models.User, req *UserListReq) (*vo.PageResp, error) {
 	req.Ensure()
 
 	o := orm.NewOrm()
@@ -184,6 +307,7 @@ func (c *Service) GetUsers(current *models.User, req *vo.PageReq) (*vo.PageResp,
 	if !current.IsAdmin() {
 		qs = qs.Filter("Uid", current.Account)
 	}
+	qs.Filter("ServerKey", req.ServerKey)
 
 	total, err := qs.Count()
 	if err != nil {

+ 7 - 18
server/modules/ldap/user_controller.go

@@ -1,20 +1,18 @@
 package ldap
 
 import (
-	"nginx-ui/server/controllers"
+	"nginx-ui/server/base"
 	"nginx-ui/server/models"
-	ngx "nginx-ui/server/nginx"
-	"nginx-ui/server/vo"
 )
 
 type UserController struct {
-	controllers.BaseController
+	base.Controller
 }
 
 func (c *UserController) Login() {
 
 	req := LDAPLoginReq{}
-	if c.ReadBody(req) == false {
+	if c.ReadBody(&req) == false {
 		return
 	}
 	user, err := ServiceInstance.Login(&req)
@@ -23,7 +21,7 @@ func (c *UserController) Login() {
 		return
 	}
 	user.Password = ""
-	c.SetSession("user", user)
+	c.SetSession("user", *user)
 	c.PostJson(models.SuccessResp(user))
 }
 
@@ -35,7 +33,7 @@ func (c *UserController) SyncUsers() {
 		return
 	}
 	req := LDAPUserSyncReq{}
-	if !c.ReadBody(req) {
+	if !c.ReadBody(&req) {
 		return
 	}
 	count, err := ServiceInstance.SyncUsers(current, &req)
@@ -52,15 +50,6 @@ func (c *UserController) SyncUsers() {
 // post /ldap/user/modifyPassword
 func (c *UserController) UpdateUserPassword() {
 
-	nginx, err := c.CheckNginxPermission()
-	if err != nil {
-		return
-	}
-
-	instance := ngx.GetInstance(nginx)
-	err = instance.Start()
-	isRun, msg := instance.Status()
-	c.SetData(isRun).SetMsg(msg).Json()
 }
 
 // UpdateUser 更新用户信息或者新增用户
@@ -76,8 +65,8 @@ func (c *UserController) GetUsers() {
 	if current == nil {
 		return
 	}
-	req := vo.PageReq{}
-	if !c.ReadBody(req) {
+	req := UserListReq{}
+	if !c.ReadBody(&req) {
 		return
 	}
 	resp, err := ServiceInstance.GetUsers(current, &req)

+ 23 - 3
server/modules/ldap/vo.go

@@ -1,13 +1,33 @@
 package ldap
 
+import "nginx-ui/server/vo"
+
 // LDAPLoginReq LDAP登录
 type LDAPLoginReq struct {
-	Server   string `json:"server"`
-	Account  string `json:"account"`
-	Password string `json:"password"`
+	ServerKey string `json:"serverKey"`
+	Account   string `json:"account"`
+	Password  string `json:"password"`
 }
 
 type LDAPUserSyncReq struct {
 	Filter    string `json:"filter"`
 	ServerKey string `json:"serverKey"`
 }
+
+type VerifyReq struct {
+	Id       int    `json:"id"`
+	Active   bool   `json:"active"`
+	Filter   string `json:"filter"` // 搜索指定的账号
+	Username string `json:"username"`
+}
+
+type UserListReq struct {
+	*vo.PageReq
+	ServerKey string `json:"serverKey"`
+}
+
+type UpdatePasswordReq struct {
+	Account     string `json:"account"`
+	Password    string `json:"password"`
+	OldPassword string `json:"oldPassword"`
+}

+ 6 - 1
server/controllers/check.go → server/modules/nginx/nginx_controller/base.go

@@ -1,14 +1,19 @@
-package controllers
+package nginx_controller
 
 import (
 	"errors"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
+	"nginx-ui/server/base"
 	"nginx-ui/server/middleware"
 	"nginx-ui/server/models"
 	"strconv"
 )
 
+type BaseController struct {
+	base.Controller
+}
+
 // CheckNginxPermission 从path中获取nginx的参数
 func (c *BaseController) CheckNginxPermission() (*models.Nginx, error) {
 	idStr := c.GetParam(":id")

+ 1 - 1
server/controllers/certificate.go → server/modules/nginx/nginx_controller/certificate.go

@@ -1,4 +1,4 @@
-package controllers
+package nginx_controller
 
 import (
 	"encoding/json"

+ 1 - 1
server/controllers/config.go → server/modules/nginx/nginx_controller/config.go

@@ -1,4 +1,4 @@
-package controllers
+package nginx_controller
 
 import (
 	"fmt"

+ 1 - 1
server/controllers/file.go → server/modules/nginx/nginx_controller/file.go

@@ -1,4 +1,4 @@
-package controllers
+package nginx_controller
 
 import (
 	"encoding/json"

+ 1 - 1
server/controllers/logger.go → server/modules/nginx/nginx_controller/logger.go

@@ -1,4 +1,4 @@
-package controllers
+package nginx_controller
 
 import (
 	"encoding/json"

+ 5 - 6
server/controllers/nginx.go → server/modules/nginx/nginx_controller/nginx.go

@@ -1,9 +1,10 @@
-package controllers
+package nginx_controller
 
 import (
 	"encoding/json"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
+	"nginx-ui/server/config"
 	"nginx-ui/server/models"
 	ngx "nginx-ui/server/nginx"
 )
@@ -12,8 +13,6 @@ type NginxController struct {
 	BaseController
 }
 
-const ReplacePassword = "******"
-
 // Get getAll,
 // 管理员获取全部,非管理员或者自己名下的
 func (c *NginxController) Get() {
@@ -31,7 +30,7 @@ func (c *NginxController) Get() {
 	for i := range list {
 		item := list[i]
 		if item.Password != "" {
-			item.Password = ReplacePassword
+			item.Password = config.ReplacePassword
 		}
 	}
 	if err != nil {
@@ -112,7 +111,7 @@ func (c *NginxController) Update() {
 	nginx.Check()
 	o := orm.NewOrm()
 
-	if nginx.Password == ReplacePassword {
+	if nginx.Password == config.ReplacePassword {
 		nginx.Password = exist.Password
 	}
 	nginx.HttpConf = exist.HttpConf
@@ -216,7 +215,7 @@ func (c *NginxController) GetNginx() {
 		return
 	}
 	if nginx.Password != "" {
-		nginx.Password = ReplacePassword
+		nginx.Password = config.ReplacePassword
 	}
 	c.AddRespData("nginx", nginx)
 

+ 1 - 1
server/controllers/server.go → server/modules/nginx/nginx_controller/server.go

@@ -1,4 +1,4 @@
-package controllers
+package nginx_controller
 
 import (
 	"encoding/json"

+ 5 - 6
server/service/nginx.go → server/modules/nginx/nginx_service/nginx.go

@@ -1,10 +1,11 @@
-package service
+package nginx_service
 
 import (
 	"encoding/json"
 	"errors"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
+	"nginx-ui/server/config"
 	"nginx-ui/server/models"
 	ngx "nginx-ui/server/nginx"
 	"strconv"
@@ -13,8 +14,6 @@ import (
 type NginxService struct {
 }
 
-const ReplacePassword = "******"
-
 // CheckNginxPermission 从path中获取nginx的参数
 func (c *NginxService) CheckNginxPermission(user *models.User, nginxId string) (*models.Nginx, error) {
 	id, err := strconv.Atoi(nginxId)
@@ -59,7 +58,7 @@ func (c *NginxService) ListNginx(current *models.User) *models.RespData {
 	for i := range list {
 		item := list[i]
 		if item.Password != "" {
-			item.Password = ReplacePassword
+			item.Password = config.ReplacePassword
 		}
 	}
 	if err != nil {
@@ -135,7 +134,7 @@ func (c *NginxService) Update(nginxId string, current *models.User, req []byte)
 	nginx.Check()
 	o := orm.NewOrm()
 
-	if nginx.Password == ReplacePassword {
+	if nginx.Password == config.ReplacePassword {
 		nginx.Password = exist.Password
 	}
 	nginx.HttpConf = exist.HttpConf
@@ -246,7 +245,7 @@ func (c *NginxService) GetNginx(nginxId string, user *models.User) *models.RespD
 		return models.NewErrorResp(err)
 	}
 	if nginx.Password != "" {
-		nginx.Password = ReplacePassword
+		nginx.Password = config.ReplacePassword
 	}
 	var resp = map[string]interface{}{}
 	resp["nginx"] = nginx

+ 8 - 7
server/controllers/oauth2.go → server/modules/oauth2/controller.go

@@ -1,4 +1,4 @@
-package controllers
+package oauth2
 
 import (
 	"context"
@@ -7,23 +7,24 @@ import (
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
 	"io"
+	"nginx-ui/server/base"
 	"nginx-ui/server/config"
 	"nginx-ui/server/models"
 	"nginx-ui/server/utils"
 )
 
-type Oauth2Controller struct {
-	BaseController
+type Controller struct {
+	*base.Controller
 }
 
-type Oauth2SSOReq struct {
+type SSOReq struct {
 	Code  string `Json:"code"`
 	Scope string `Json:"scope"`
 	State string `Json:"state"`
 }
 
 // Get 获取oauth2.0的登录url
-func (c *Oauth2Controller) Get() {
+func (c *Controller) Get() {
 	state, err := utils.RandPassword(6)
 	if err != nil {
 		c.ErrorJson(err)
@@ -34,8 +35,8 @@ func (c *Oauth2Controller) Get() {
 }
 
 // Callback 用户注册
-func (c *Oauth2Controller) Callback() {
-	var ssoReq Oauth2SSOReq
+func (c *Controller) Callback() {
+	var ssoReq SSOReq
 	err := json.Unmarshal(c.Ctx.Input.RequestBody, &ssoReq)
 	if err != nil {
 		logs.Error(err, string(c.Ctx.Input.RequestBody))

+ 109 - 0
server/modules/user/controller.go

@@ -0,0 +1,109 @@
+package user
+
+import (
+	"encoding/json"
+	"github.com/astaxie/beego/logs"
+	"nginx-ui/server/base"
+	"nginx-ui/server/models"
+	"nginx-ui/server/vo"
+)
+
+type Controller struct {
+	base.Controller
+	service *UserService
+}
+
+func NewUserController() *Controller {
+
+	return &Controller{
+		service: NewUserService(),
+	}
+}
+
+// Login 登录
+func (c *Controller) Login() {
+	var user models.User
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &user)
+	if err != nil {
+		logs.Error(err, string(c.Ctx.Input.RequestBody))
+		c.ErrorJson(err)
+		return
+	}
+	resp := c.service.Login(&user)
+	if resp.Success() {
+		c.SetSession("user", user)
+	}
+	c.PostJson(resp)
+}
+
+func (c *Controller) User() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	c.SetData(user).Json()
+}
+
+// Register 用户注册
+func (c *Controller) Register() {
+	resp := c.service.SignUp(c.Ctx.Input.RequestBody)
+	c.PostJson(resp)
+}
+
+// Users 获取全部用户信息
+func (c *Controller) Users() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := vo.PageReq{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	resp, err := c.service.Users(&req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(resp).Json()
+}
+
+// Update 获取全部用户信息
+func (c *Controller) Update() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := models.User{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	resp, err := c.service.Update(&req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(resp).Json()
+}
+
+// UpdatePassword Update 获取全部用户信息
+func (c *Controller) UpdatePassword() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := vo.UserUpdatePassword{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	req.Id = user.Id
+	err := c.service.UpdatePassword(&req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetMsg("密码更新成功").Json()
+}

+ 150 - 0
server/modules/user/service.go

@@ -0,0 +1,150 @@
+package user
+
+import (
+	"encoding/json"
+	"errors"
+	"github.com/astaxie/beego/logs"
+	"github.com/astaxie/beego/orm"
+	"nginx-ui/server/config"
+	"nginx-ui/server/models"
+	"nginx-ui/server/modules/ldap"
+	"nginx-ui/server/utils"
+	"nginx-ui/server/vo"
+)
+
+type UserService struct {
+}
+
+func NewUserService() *UserService {
+	return &UserService{}
+}
+
+func (u *UserService) Login(user *models.User) *models.RespData {
+	cipherPassword := user.Password
+	o := orm.NewOrm()
+	err := o.Read(user, "Account")
+	if err != nil {
+		return models.NewErrorResp(err)
+	}
+	encryptPassword := utils.GetSHA256HashCode(cipherPassword)
+	if encryptPassword != user.Password {
+		return models.ErrorResp("用户名或者密码不正确!")
+	}
+	user.Password = ""
+	return models.SuccessResp(user)
+}
+
+func (u *UserService) SignUp(req []byte) *models.RespData {
+
+	var user models.User
+	err := json.Unmarshal(req, &user)
+	if err != nil {
+		logs.Error(err, req)
+		return models.NewErrorResp(err)
+	}
+
+	if len(user.Account) == 0 || len(user.Password) == 0 {
+		return models.ErrorResp("账号或者密码不能为空!")
+	}
+	if len(user.Nickname) == 0 {
+		user.Nickname = user.Account
+	}
+	user.Password = utils.GetSHA256HashCode(user.Password)
+	o := orm.NewOrm()
+	_, err = o.Insert(&user)
+
+	if err != nil {
+		return models.NewErrorResp(err)
+	}
+
+	return models.SuccessResp(user).SetMsg("注册成功!")
+}
+
+func (u *UserService) Users(req *vo.PageReq) (*vo.PageResp, error) {
+	req.Ensure()
+	qs := orm.NewOrm().QueryTable(new(models.User))
+	qs = qs.Offset(req.Offset).Limit(req.PageSize)
+
+	var list []models.User
+	_, err := qs.All(&list)
+	if err != nil {
+		return nil, err
+	}
+	count, err := qs.Count()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, user := range list {
+		user.Password = config.ReplacePassword
+	}
+	resp := vo.PageResp{
+		PageSize: req.PageSize,
+		Current:  req.Current,
+		Total:    count,
+		List:     list,
+	}
+	return &resp, err
+}
+
+func (u *UserService) Update(req *models.User) (*models.User, error) {
+	o := orm.NewOrm()
+
+	exist := models.User{Id: req.Id}
+	err := o.Read(&exist)
+	if err != nil {
+		return nil, errors.New("该用户不存在或者已被删除!")
+	}
+	if req.Password == "" || req.Password == config.ReplacePassword {
+		req.Password = exist.Password
+	}
+	_, err = o.Update(req)
+	if err != nil {
+		return nil, errors.New("更新失败,请重试!")
+	}
+	return req, nil
+}
+
+// UpdatePassword 更新用户密码,如果存在LDAP账号,则更新下
+func (u *UserService) UpdatePassword(req *vo.UserUpdatePassword) error {
+	o := orm.NewOrm()
+
+	user := models.User{Id: req.Id}
+	err := o.Read(&user)
+	if err != nil {
+		return errors.New("该用户不存在或者已被删除!")
+	}
+	if user.Source == "LDAP" {
+		err = ldap.ServiceInstance.UpdateUserPassword(&ldap.UpdatePasswordReq{
+			Password:    req.NewPassword,
+			Account:     user.Account,
+			OldPassword: req.OldPassword,
+		}, false)
+		if err != nil {
+			return err
+		}
+		user.Password = req.NewPassword
+		_, err = o.Update(&user)
+		if err != nil {
+			logs.Error("update password error: %v", err)
+		}
+	} else {
+		if req.OldPassword != user.Password {
+			return errors.New("当前密码不正确!")
+		}
+		user.Password = req.NewPassword
+		_, err = o.Update(&user)
+		if err != nil {
+			return err
+		}
+		err = ldap.ServiceInstance.UpdateUserPassword(&ldap.UpdatePasswordReq{
+			Password: req.NewPassword,
+			Account:  user.Account,
+		}, true)
+		if err != nil {
+			logs.Error("LDAP updatePassword fail: %v", err)
+			return errors.New("密码更新成功,但同步LDAP失败:" + err.Error())
+		}
+	}
+	return nil
+}

+ 25 - 20
server/routers/router.go

@@ -8,10 +8,12 @@ import (
 	"github.com/astaxie/beego/logs"
 	"net/http"
 	config2 "nginx-ui/server/config"
-	"nginx-ui/server/controllers"
 	"nginx-ui/server/middleware"
 	"nginx-ui/server/models"
 	"nginx-ui/server/modules/ldap"
+	"nginx-ui/server/modules/nginx/nginx_controller"
+	"nginx-ui/server/modules/oauth2"
+	"nginx-ui/server/modules/user"
 	"strings"
 )
 
@@ -25,35 +27,38 @@ var NginxStatusR = "/nginx/:id/status"
 func init() {
 	config := config2.Config
 
-	userController := controllers.NewUserController()
+	userController := user.NewUserController()
 
 	logs.Info("baseApi", config.BaseApi)
 
 	ns := beego.NewNamespace(config.BaseApi,
-		beego.NSRouter(NginxR, &controllers.NginxController{}),
-		beego.NSRouter(NginxGetR, &controllers.NginxController{}, "post:Update"),
-		beego.NSRouter(NginxGetR, &controllers.NginxController{}, "get:GetNginx"),
-		beego.NSRouter(NginxGetR, &controllers.NginxController{}, "delete:DelNginx"),
-		beego.NSRouter(NginxRefreshR, &controllers.NginxController{}, "post:RefreshHttp"),
-		beego.NSRouter(NginxStartR, &controllers.NginxController{}, "post:StartNginx"),
-		beego.NSRouter(NginxStopR, &controllers.NginxController{}, "post:StopNginx"),
-		beego.NSRouter(NginxStatusR, &controllers.NginxController{}, "post:StatusNginx"),
+		beego.NSRouter(NginxR, &nginx_controller.NginxController{}),
+		beego.NSRouter(NginxGetR, &nginx_controller.NginxController{}, "post:Update"),
+		beego.NSRouter(NginxGetR, &nginx_controller.NginxController{}, "get:GetNginx"),
+		beego.NSRouter(NginxGetR, &nginx_controller.NginxController{}, "delete:DelNginx"),
+		beego.NSRouter(NginxRefreshR, &nginx_controller.NginxController{}, "post:RefreshHttp"),
+		beego.NSRouter(NginxStartR, &nginx_controller.NginxController{}, "post:StartNginx"),
+		beego.NSRouter(NginxStopR, &nginx_controller.NginxController{}, "post:StopNginx"),
+		beego.NSRouter(NginxStatusR, &nginx_controller.NginxController{}, "post:StatusNginx"),
 		// certs
-		beego.NSRouter("/nginx/:id/certs", &controllers.CertController{}),
-		beego.NSRouter("/nginx/:id/certs/sync", &controllers.CertController{}, "post:Sync"),
+		beego.NSRouter("/nginx/:id/certs", &nginx_controller.CertController{}),
+		beego.NSRouter("/nginx/:id/certs/sync", &nginx_controller.CertController{}, "post:Sync"),
 		// nginx server apis
-		beego.NSRouter("/nginx/:id/server", &controllers.ServerController{}),
-		beego.NSRouter("/nginx/:id/server/refresh", &controllers.ServerController{}, "post:Refresh"),
+		beego.NSRouter("/nginx/:id/server", &nginx_controller.ServerController{}),
+		beego.NSRouter("/nginx/:id/server/refresh", &nginx_controller.ServerController{}, "post:Refresh"),
 		// file upload download
-		beego.NSRouter("/nginx/:id/file/deploy", &controllers.FileController{}, "post:Deploy"),
-		beego.NSRouter("/file", &controllers.FileController{}),
-		beego.NSRouter("/logger", &controllers.LoggerController{}),
+		beego.NSRouter("/nginx/:id/file/deploy", &nginx_controller.FileController{}, "post:Deploy"),
+		beego.NSRouter("/file", &nginx_controller.FileController{}),
+		beego.NSRouter("/logger", &nginx_controller.LoggerController{}),
 
 		beego.NSRouter("/user/login", userController, "post:Login"),
 		beego.NSRouter("/user/info", userController, "get:User"),
 		beego.NSRouter("/user/register", userController, "post:Register"),
-		beego.NSRouter("/oauth2", &controllers.Oauth2Controller{}),
-		beego.NSRouter("/oauth2/callback", &controllers.Oauth2Controller{}, "post:Callback"),
+		beego.NSRouter("/user/list", userController, "post:Users"),
+		beego.NSRouter("/user/update", userController, "post:Update"),
+		beego.NSRouter("/user/modifyPassword", userController, "post:UpdatePassword"),
+		beego.NSRouter("/oauth2", &oauth2.Controller{}),
+		beego.NSRouter("/oauth2/callback", &oauth2.Controller{}, "post:Callback"),
 	)
 	beego.AddNamespace(ns)
 	// LDAP路由
@@ -61,7 +66,7 @@ func init() {
 
 	beego.InsertFilter(fmt.Sprintf("%s/**", config.BaseApi), beego.BeforeRouter, middleware.AuthFilter)
 
-	beego.Router(fmt.Sprintf("%s/config.js", config.ContextPath), &controllers.ConfigController{})
+	beego.Router(fmt.Sprintf("%s/config.js", config.ContextPath), &nginx_controller.ConfigController{})
 	// portal static assets
 	beego.SetStaticPath(config.ContextPath, "static/web")
 	beego.Get("/", func(ctx *context.Context) {

+ 0 - 57
server/service/user.go

@@ -1,57 +0,0 @@
-package service
-
-import (
-	"encoding/json"
-	"github.com/astaxie/beego/logs"
-	"github.com/astaxie/beego/orm"
-	"nginx-ui/server/models"
-	"nginx-ui/server/utils"
-)
-
-type UserService struct {
-}
-
-func NewUserService() *UserService {
-	return &UserService{}
-}
-
-func (u *UserService) Login(user *models.User) *models.RespData {
-	cipherPassword := user.Password
-	o := orm.NewOrm()
-	err := o.Read(user, "Account")
-	if err != nil {
-		return models.NewErrorResp(err)
-	}
-	encryptPassword := utils.GetSHA256HashCode(cipherPassword)
-	if encryptPassword != user.Password {
-		return models.ErrorResp("用户名或者密码不正确!")
-	}
-	user.Password = ""
-	return models.SuccessResp(user)
-}
-
-func (u *UserService) SignUp(req []byte) *models.RespData {
-
-	var user models.User
-	err := json.Unmarshal(req, &user)
-	if err != nil {
-		logs.Error(err, req)
-		return models.NewErrorResp(err)
-	}
-
-	if len(user.Account) == 0 || len(user.Password) == 0 {
-		return models.ErrorResp("账号或者密码不能为空!")
-	}
-	if len(user.Nickname) == 0 {
-		user.Nickname = user.Account
-	}
-	user.Password = utils.GetSHA256HashCode(user.Password)
-	o := orm.NewOrm()
-	_, err = o.Insert(&user)
-
-	if err != nil {
-		return models.NewErrorResp(err)
-	}
-
-	return models.SuccessResp(user).SetMsg("注册成功!")
-}

+ 61 - 54
server/utils/file.go

@@ -1,54 +1,61 @@
-package utils
-
-import (
-	"context"
-	"github.com/mholt/archiver/v4"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-func IsExist(path string) bool {
-	_, err := os.Stat(path)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return true
-}
-
-func TarXz(dst string, src string) error {
-	src = filepath.Clean(src)
-	dst = filepath.Clean(dst)
-	if !strings.HasSuffix(src, string(os.PathSeparator)) {
-		src += string(os.PathSeparator)
-	}
-	files, err := archiver.FilesFromDisk(nil, map[string]string{
-		src: "",
-	})
-	if err != nil {
-		return err
-	}
-
-	out, err := os.Create(dst)
-	if err != nil {
-		return err
-	}
-	defer out.Close()
-	var compression archiver.Compression
-	if strings.HasSuffix(dst, "gz") {
-		compression = archiver.Gz{}
-	} else if strings.HasSuffix(dst, "xz") {
-		compression = archiver.Xz{}
-	} else {
-		compression = archiver.Gz{}
-	}
-	format := archiver.CompressedArchive{
-		Compression: compression,
-		Archival:    archiver.Tar{},
-	}
-	err = format.Archive(context.Background(), out, files)
-	if err != nil {
-		return err
-	}
-	return nil
-}
+package utils
+
+import (
+	"context"
+	"crypto/md5"
+	"encoding/hex"
+	"github.com/mholt/archiver/v4"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func IsExist(path string) bool {
+	_, err := os.Stat(path)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return true
+}
+
+func TarXz(dst string, src string) error {
+	src = filepath.Clean(src)
+	dst = filepath.Clean(dst)
+	if !strings.HasSuffix(src, string(os.PathSeparator)) {
+		src += string(os.PathSeparator)
+	}
+	files, err := archiver.FilesFromDisk(nil, map[string]string{
+		src: "",
+	})
+	if err != nil {
+		return err
+	}
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+	var compression archiver.Compression
+	if strings.HasSuffix(dst, "gz") {
+		compression = archiver.Gz{}
+	} else if strings.HasSuffix(dst, "xz") {
+		compression = archiver.Xz{}
+	} else {
+		compression = archiver.Gz{}
+	}
+	format := archiver.CompressedArchive{
+		Compression: compression,
+		Archival:    archiver.Tar{},
+	}
+	err = format.Archive(context.Background(), out, files)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func CalcMd5(content string) string {
+	pass := md5.Sum([]byte(content))
+	return hex.EncodeToString(pass[:])
+}

+ 8 - 0
server/vo/user.go

@@ -0,0 +1,8 @@
+package vo
+
+// UserUpdatePassword 修改密码请求
+type UserUpdatePassword struct {
+	Id          int    `json:"id"`
+	OldPassword string `json:"oldPassword"`
+	NewPassword string `json:"newPassword"`
+}