Explorar o código

feat: 新增LDAP用户-50%

tuonian hai 4 semanas
pai
achega
266b17c812

+ 13 - 2
frontend/src/api/ldap.ts

@@ -24,11 +24,15 @@ export namespace LDAP {
         userName: string
         mail: string
         dn: string
-        attributes?: string
+        attributes?: string | {Name: string,Value?: any}[]
         signType: string
         serverKey: string
         remark?: string
         password?: string
+        // 以下非数据库字段
+        givenName?: string
+        sn?: string
+        objectClass?: string
     }
 
     export type VerifyData = {
@@ -84,11 +88,18 @@ export const LDAPApis = {
         return request.post<PageResp<LDAP.User>>('/ldap/user/list', req)
     },
     getUserDetail: (id: number) =>{
-        return request.post<BaseResp<LDAP.User>>('/ldap/user/detail', { id: id })
+        return request.get<BaseResp<LDAP.User>>('/ldap/user/detail', { params: { id }})
     },
     saveUser: (user: Partial<LDAP.User>) => {
         return request.post<BaseResp<LDAP.User>>(`/ldap/user/save`, user)
     },
+    /**
+     * 新增用户
+     * @param user
+     */
+    add: (user: Partial<LDAP.User>) => {
+        return request.post<BaseResp<LDAP.User>>(`/ldap/user/add`, user)
+    },
     syncUsers: (search: any) => {
         return request.post<BaseResp<{ count: number}>>('/ldap/user/sync', search)
     }

+ 6 - 3
frontend/src/components/curd/Form.tsx

@@ -1,6 +1,6 @@
 import {Button} from "antd";
 import {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
-import {AutoForm, AutoFormInstance, isNullOrTrue, Message} from "auto-antd";
+import {AutoForm, AutoFormInstance, isNull, isNullOrTrue, Message} from "auto-antd";
 import {CurdColumn} from "./index.tsx";
 import {FormColumnType} from "auto-antd/dist/esm/Model";
 import {ICurdConfig, ICurdData} from "./types.ts";
@@ -32,12 +32,15 @@ const Form = <D extends ICurdData,>({config, initialData, onClose, editable,...p
 
 
     const formColumns = useMemo(()=>{
-        return props.columns.filter(item=>isNullOrTrue(item.addable))
+        const isAdd = isNull(initialData?.id)
+        return props.columns.filter(item=>{
+            return (isAdd && isNullOrTrue(item.addable)) || (!isAdd && !item.onlyAdd && !item.editHide)
+        })
             .map(item=>{
             return {
                 ...item,
                 width: undefined,
-                editable: isNullOrTrue(editable) ? initialData?.id ? item.editable : item.addable : false,
+                editable: isAdd ? isNullOrTrue(item.addable) : isNullOrTrue(editable) && item.editable
             }
         }) as FormColumnType[]
     },[props.columns, initialData,editable])

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

@@ -14,6 +14,9 @@
         .ant-input-password{
           max-width: unset;
         }
+        .input-item{
+          max-width: 100%;
+        }
       }
     }
   }

+ 3 - 1
frontend/src/components/curd/index.tsx

@@ -11,9 +11,11 @@ import {ICurdConfig, ICurdData} from "./types.ts";
 
 export type CurdColumn = AutoColumn & {
     search?: boolean
-    editable?: boolean
+    editable?: boolean // 编辑时是否可编辑
     hidden?: boolean
     addable?: boolean // 添加时可编辑
+    onlyAdd?: boolean // 仅新增时展示
+    editHide?: boolean // 编辑时隐藏
 }
 
 

+ 70 - 7
frontend/src/pages/ldap/user/components/UserAttributes.tsx

@@ -1,12 +1,75 @@
-import {LDAP} from "../../../../api/ldap.ts";
+import {ArrayInput, AutoColumn, AutoTypeInputProps, ColumnModel} from "auto-antd";
+import {Popover} from "antd";
+import {useEffect, useRef, useState} from "react";
 
+const column = {
+    key: "attributes",
+    type: 'array',
+    items: [
+        {
+            key: 'Name',
+            title: '属性名称',
+            type: 'string',
+            ruleType: 'unique'
+        },
+        {
+            key: 'Value',
+            title: "属性值",
+            type: 'string',
+        }
+    ] as ColumnModel[]
 
-type IProps = {
-    user: LDAP.User
+} as AutoColumn
+
+
+
+type Attr = {
+    Name: string
+    Value?: string
 }
 
-export const UserAttributes = ({}: IProps) => {
-    return <>
-        <span>查看详情</span>
-    </>
+
+export const UserAttributes = ({ value, ...props }: AutoTypeInputProps) => {
+
+    const [attrs, setAttrs] = useState<Attr[]>([])
+    const source = useRef<any>()
+
+    useEffect(() => {
+        if (source.current == value){
+            return
+        }
+        if (!value){
+            setAttrs([])
+            return;
+        }
+        try {
+            setAttrs(value)
+        }catch (e){
+            console.error('[UserAttributes] parse',e)
+            setAttrs([])
+        }
+    }, [value]);
+
+    const onChange = (list: any[]) => {
+        setAttrs(list)
+        source.current = list
+        props.onChange?.(source.current)
+    }
+
+
+    return <div className="attr-list">
+        <ArrayInput column={column} value={attrs} onChange={onChange} renderHeader={true}/>
+    </div>
+}
+
+
+export const UserAttributesText = ({value}: {value: string}) => {
+
+    return (
+        <>
+            <Popover title="查看详情" content={<div>{value}</div>}>
+                <span>查看</span>
+            </Popover>
+        </>
+    )
 }

+ 4 - 0
frontend/src/pages/ldap/user/index.tsx

@@ -4,6 +4,10 @@ import {Menu, MenuProps} from "antd";
 import {BackButton} from "../../../components/BackButton.tsx";
 import {LDAP} from "../../../api/ldap.ts";
 import './index.less'
+import {registerInput} from "../../nginx/components/basic";
+import {UserAttributes} from "./components/UserAttributes.tsx";
+
+registerInput('attributes', UserAttributes);
 
 export const UserLayout = () => {
 

+ 126 - 15
frontend/src/pages/ldap/user/list/index.tsx

@@ -7,8 +7,9 @@ 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";
+import {isNullOrTrue, Message} from "auto-antd";
+import {UserAttributesText} from "../components/UserAttributes.tsx";
+import {safeParse} from "../../../../utils/json.ts";
 
 const columns: CurdColumn[] = [
     {
@@ -17,23 +18,67 @@ const columns: CurdColumn[] = [
         type: 'string',
         disabled: true,
         required: false,
-        width: 50
+        width: 50,
+        addable: false,
     },
     {
         key: 'account',
         title: '账号(uid)',
         type: 'string',
-        disabled: true,
-        placeholder: '服务唯一编码,自动生成',
-        required: false,
-        width: 120
+        editable: false,
+        placeholder: '账号,全局唯一,不可重复',
+        required: true,
+        width: 120,
+        addable: true,
+    },
+    {
+        key: 'givenName',
+        title: '姓',
+        type: 'string',
+        editable: true,
+        required: true,
+        width: 120,
+        hidden: true,
+        addable: true,
+    },
+    {
+        key: 'sn',
+        title: '名',
+        type: 'string',
+        editable: true,
+        required: true,
+        width: 120,
+        hidden: true,
+        addable: true,
+    },
+    {
+        key: 'objectClass',
+        title: 'objectClass',
+        type: 'select',
+        editable: true,
+        required: true,
+        width: 120,
+        hidden: true,
+        addable: true,
+        option:['top','inetOrgPerson','organizationalPerson','person'],
+        multiple: true,
     },
     {
         key: 'dn',
         title: 'DN',
         type: 'string',
         placeholder: 'eg. cn=Abc,dc=tonyandmoney,dc=cn',
-        width: 150
+        width: 150,
+        addable: false,
+        editable: false,
+        editHide: true,
+    },
+    {
+        key: 'organize',
+        title: '上级(DN)',
+        type: 'string',
+        placeholder: 'eg. ou=users,dc=tonyandmoney,dc=cn',
+        width: 150,
     },
     {
         key: 'mail',
@@ -41,11 +86,20 @@ const columns: CurdColumn[] = [
         type: 'string',
         width: 150
     },
+    {
+        key: 'password',
+        title: '密码',
+        type: 'password',
+        width: 150,
+        addable: true,
+    },
     {
         key: 'attributes',
-        title: '详细属性',
-        type: 'string',
-        render: (_: any, record: LDAP.User) => <UserAttributes user={record} />
+        title: '更多属性',
+        type: 'attributes',
+        render: (v: string)=>(<UserAttributesText  value={v}/>),
+        hidden: true,
+        required: false,
     },
     {
         key: 'remark',
@@ -55,9 +109,57 @@ const columns: CurdColumn[] = [
     }
 ]
 
+const toObjKeys = ['givenName','sn', 'objectClass']
+const delKeys = ['cn','mail','userPassword','uid']
+
+const parseUser = (user: LDAP.User) => {
+    const attrs = safeParse(user.attributes, [])
+    console.log('attrs', attrs)
+    const attrList = []
+    for (const attr of attrs) {
+        attr.Value = Array.isArray(attr.Values) ? attr.Values[0] : undefined
+        if (attr.Name == 'objectClass'){
+            attr.Value = attr.Values || []
+        }
+        if (toObjKeys.includes(attr.Name)){
+            (user as any)[attr.Name] = attr.Value
+        }else if (!delKeys.includes(attr.Name)){
+            attrList.push(attr)
+        }
+    }
+    return {
+        ...user,
+        attributes: attrList,
+    } as LDAP.User
+}
+
+const stringifyUser = (user: Partial<LDAP.User>) => {
+    const attributes = []
+    for (const attr of (user.attributes as any[])) {
+        attributes.push({
+            Name: attr.Name,
+            Values: isNullOrTrue(attr.Value) ? [attr.Values] : []
+        })
+    }
+    for (const k of toObjKeys){
+        const v = (user as any)[k]
+        if (isNullOrTrue(v)) {
+            attributes.push({
+                Name: k,
+                Values: [v]
+            })
+        }
+    }
+    return {
+        ...user,
+        attributes: JSON.stringify(attributes),
+    }
+
+}
+
 const serverConfig: ICurdConfig<LDAP.User> = {
-    editDialogWidth: 500,
-    labelSpan: 6,
+    editDialogWidth: 700,
+    labelSpan: 4,
 }
 
 /**
@@ -89,14 +191,23 @@ export const List = () => {
         }
         return LDAPApis.getUserDetail(data.id).then(res => {
             if (res.data?.data) {
-                return res.data.data;
+                const user = parseUser(res.data.data);
+                console.log('user', user)
+                return user;
             }
             throw new Error('该服务不存在!')
         })
     }
 
     const onSaveUser = (data: Partial<LDAP.User>) => {
-        return LDAPApis.saveUser(data)
+        const user = stringifyUser(data)
+        if (data.id){
+            return LDAPApis.saveUser(user)
+                .then(res => {
+                    return res.data.data as LDAP.User;
+                })
+        }
+        return LDAPApis.add(user)
             .then(res => {
                 return res.data.data as LDAP.User;
             })

+ 1 - 0
frontend/src/pages/nginx/layout.tsx

@@ -25,6 +25,7 @@ const nginxRoutes: INginxRoute[] = [
     {
         path: "",
         component: NginxList,
+        index: true,
     },
     {
         path: ':id',

+ 5 - 1
frontend/src/routes/index.tsx

@@ -16,6 +16,7 @@ import {NginxLayout} from "../pages/nginx/layout.tsx";
 import {ldapRoutes} from '../pages/ldap/layout.tsx'
 import {ErrorBoundary} from "../components/error/ErrorBoundary.tsx";
 import {UserList} from "../pages/user/list";
+import {DirectPage} from "./routes.tsx";
 /**
  * @author tuonian
  * @date 2023/6/26
@@ -84,7 +85,6 @@ const router = createHashRouter([
             {
                 path: 'nginx/*',
                 Component: NginxLayout,
-                index: true,
                 // handle: {
                 //     crumb: true,
                 //     label: 'Nginx列表'
@@ -95,6 +95,10 @@ const router = createHashRouter([
                 path: 'user/list',
                 Component: UserList,
             },
+            {
+                index: true,
+                element: <DirectPage to="/nginx" />
+            }
         ],
         errorElement: <ErrorBoundary />
     },

+ 15 - 0
frontend/src/routes/routes.tsx

@@ -1,3 +1,7 @@
+import {LoadingText} from "../components/loading";
+import {useEffect} from "react";
+import {useNavigate} from "react-router";
+
 /**
  * 顶部的菜单
  */
@@ -18,3 +22,14 @@ export const NavList = [
     roles: ['ADMIN']
   }
 ]
+
+type IProps = {
+  to: string
+}
+export const DirectPage = ({to}: IProps) => {
+  const navigate = useNavigate()
+  useEffect(() => {
+    navigate(to)
+  }, [navigate, to]);
+  return <LoadingText />
+}

+ 12 - 0
frontend/src/utils/json.ts

@@ -0,0 +1,12 @@
+/**
+ * 安全解析,捕获异常,返回默认值
+ * @param json
+ * @param def
+ */
+export const safeParse = (json: any, def={}) => {
+    try {
+        return JSON.parse(json);
+    }catch(e){
+        return def;
+    }
+}

+ 25 - 20
server/models/ldap.go

@@ -1,19 +1,22 @@
 package models
 
+import "time"
+
 // LdapServer LDAP 服务配置表
 type LdapServer struct {
 	Id int `orm:"pk;auto" json:"id"`
 	// 用户账号,唯一标识, Uid
-	Uid      string `orm:"size(100)" json:"uid"`
-	Name     string `orm:"size(255)" json:"name"`
-	Key      string `orm:"unique" json:"key"`
-	Url      string `json:"url"`
-	Active   bool   `json:"active"`   // 是否激活可用
-	UserName string `json:"userName"` // 直接存储JSON数据,数组格式,多个
-	Password string `json:"password"`
-	BaseDN   string `orm:"size(255);column(base_dn)" json:"baseDN"`
-	Filter   string `orm:"size(255)" json:"filter"`
-	Remark   string `json:"remark"`
+	Uid           string `orm:"size(100)" json:"uid"`
+	Name          string `orm:"size(255)" json:"name"`
+	Key           string `orm:"unique" json:"key"`
+	Url           string `json:"url"`
+	Active        bool   `json:"active"`   // 是否激活可用
+	UserName      string `json:"userName"` // 直接存储JSON数据,数组格式,多个
+	Password      string `json:"password"`
+	BaseDN        string `orm:"size(255);column(base_dn)" json:"baseDN"`
+	Filter        string `orm:"size(255)" json:"filter"`
+	Remark        string `json:"remark"`
+	OrganizeClass string `orm:"size(255)" json:"organizeClass"` // 组织的objectClass
 }
 
 // LdapUser User 用户表
@@ -21,14 +24,16 @@ type LdapServer struct {
 type LdapUser struct {
 	Id int `orm:"pk;auto" json:"id"`
 	// 用户账号,唯一标识, Uid
-	Uid        string `orm:"unique" json:"uid"`
-	Account    string `json:"account"` // 即DN,eg. cn=test,dc=xxxx,dc=cn
-	UserName   string `json:"userName"`
-	Mail       string `json:"mail"`
-	DN         string `orm:"column(db)" json:"dn"`
-	Attributes string `orm:"null;type(text)" json:"attributes"` // 直接存储JSON数据,数组格式,多个
-	SignType   string `json:"signType"`                         // 密码加密方式
-	Password   string `json:"password"`
-	Remark     string `json:"remark"`
-	ServerKey  string `json:"serverKey"`
+	Uid          string    `orm:"unique" json:"uid"`
+	Account      string    `json:"account"` // 即DN,eg. cn=test,dc=xxxx,dc=cn
+	UserName     string    `json:"userName"`
+	Mail         string    `json:"mail"`
+	DN           string    `orm:"column(db)" json:"dn"`
+	Attributes   string    `orm:"null;type(text)" json:"attributes"` // 直接存储JSON数据,数组格式,多个
+	SignType     string    `json:"signType"`                         // 密码加密方式
+	Password     string    `json:"password"`
+	Remark       string    `json:"remark"`
+	ServerKey    string    `json:"serverKey"`
+	LastSyncDate time.Time `orm:"type(datetime)" json:"lastSyncDate"` // 最后一次同步的时间
+	Organize     string    `orm:"default('');type(text)" json:"organize"`
 }

+ 19 - 0
server/modules/ldap/client.go

@@ -172,6 +172,25 @@ func (c *Client) Modify() error {
 	return nil
 }
 
+// Add 新增用户
+func (c *Client) Add(user *models.LdapUser) error {
+	var attrs []ldap.Attribute
+	err := json.Unmarshal([]byte(user.Attributes), &attrs)
+	if err != nil {
+		return err
+	}
+
+	request := ldap.AddRequest{
+		DN:         user.DN,
+		Attributes: attrs,
+	}
+	err = c.Conn.Add(&request)
+	if err != nil {
+		logs.Error("Add fail: %v", err)
+	}
+	return err
+}
+
 func (c *Client) Authentication(account string, password string) (*models.LdapUser, error) {
 	// The username and password we want to check
 	users, err := c.Search(fmt.Sprintf("(&(objectClass=*)(uid=%s))", ldap.EscapeFilter(account)))

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

@@ -17,6 +17,8 @@ func InitRouter(prefix string) *beego.Namespace {
 		beego.NSRouter("/user/sync", &UserController{}, "post:SyncUsers"),
 		beego.NSRouter("/user/updatePassword", &UserController{}, "post:UpdateUserPassword"),
 		beego.NSRouter("/user/update", &UserController{}, "post:UpdateUser"),
+		beego.NSRouter("/user/detail", &UserController{}, "get:GetDetail"),
+		beego.NSRouter("/user/add", &UserController{}, "post:AddUser"),
 		beego.NSRouter("/user/list", &UserController{}, "post:GetUsers"),
 	)
 	return ns

+ 0 - 2
server/modules/ldap/server_controller.go

@@ -13,8 +13,6 @@ type ServerController struct {
 	base.Controller
 }
 
-var ServiceInstance = new(Service)
-
 // GetServer 获取一个可用的LDAP 连接, 用于登录时获取服务信息
 func (c *ServerController) GetServer() {
 	server, err := ServiceInstance.GetServer()

+ 47 - 0
server/modules/ldap/service.go

@@ -15,6 +15,9 @@ import (
 type Service struct {
 }
 
+var ServiceInstance = new(Service)
+var UserIns = new(UserService)
+
 // GetServer 获取一个可用的LDAP 连接, 用于登录时获取服务信息
 func (c *Service) GetServer() (*models.LdapServer, error) {
 	o := orm.NewOrm()
@@ -227,6 +230,50 @@ func (c *Service) Update(current *models.User, body *models.LdapServer) (*models
 	return body, nil
 }
 
+// Add Update 保存或者修改
+func (c *Service) Add(current *models.User, body *models.LdapServer) (*models.LdapServer, error) {
+
+	if body.Url == "" {
+		return nil, errors.New("请完成服务配置,缺少Url!")
+	}
+	if body.Key == "" {
+		key := md5.Sum([]byte(body.Url))
+		body.Key = hex.EncodeToString(key[:])
+	}
+	o := orm.NewOrm()
+	if body.Id == 0 {
+		exist := models.LdapServer{Key: body.Key}
+		err := o.Read(&exist, "Key")
+		if err != nil && !errors.Is(err, orm.ErrNoRows) {
+			return nil, err
+		}
+		if exist.Id > 0 {
+			return nil, errors.New("该服务Url已存在!")
+		}
+	}
+	if body.Id > 0 {
+		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)
+		if err != nil {
+			return nil, err
+		}
+		body.Id = int(id)
+	}
+	return body, nil
+}
+
 // VerifyServer 验证服务
 func (c *Service) VerifyServer(req *VerifyReq) ([]models.LdapUser, error) {
 	var server = &models.LdapServer{

+ 38 - 1
server/modules/ldap/user_controller.go

@@ -53,11 +53,29 @@ func (c *UserController) UpdateUserPassword() {
 }
 
 // UpdateUser 更新用户信息或者新增用户
-// post /ldap/user/save
+// post /ldap/user/update
 func (c *UserController) UpdateUser() {
 
 }
 
+// AddUser 新增用户
+func (c *UserController) AddUser() {
+	current := c.RequiredUser()
+	if current == nil {
+		return
+	}
+	user := models.LdapUser{}
+	if !c.ReadBody(&user) {
+		return
+	}
+	resp, err := UserIns.Add(&user)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(resp).Json()
+}
+
 // GetUsers 获取全部用户
 // get /ldap/users
 func (c *UserController) GetUsers() {
@@ -76,3 +94,22 @@ func (c *UserController) GetUsers() {
 	}
 	c.SetData(resp).Json()
 }
+
+func (c *UserController) GetDetail() {
+	current := c.RequiredUser()
+	if current == nil {
+		return
+	}
+	id, err := c.GetIntQuery("id")
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+
+	resp, err := UserIns.GetDetail(id)
+	if err != nil {
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(resp).Json()
+}

+ 55 - 0
server/modules/ldap/user_service.go

@@ -0,0 +1,55 @@
+package ldap
+
+import (
+	"github.com/astaxie/beego/orm"
+	"nginx-ui/server/config"
+	"nginx-ui/server/models"
+	"time"
+)
+
+type UserService struct {
+}
+
+// Add Update 保存或者修改
+func (c *UserService) Add(body *models.LdapUser) (*models.LdapUser, error) {
+
+	server := models.LdapServer{
+		Key: body.ServerKey,
+	}
+	o := orm.NewOrm()
+	err := o.Read(&server, "Key")
+	if err != nil {
+		return nil, err
+	}
+	body.Uid = server.Uid
+
+	_, err = o.Insert(body)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := GetActiveClient(&server)
+	if err != nil {
+		return nil, err
+	}
+	err = client.Add(body)
+	if err != nil {
+		return nil, err
+	}
+	body.LastSyncDate = time.Now()
+	_, _ = o.Update(body)
+	return body, nil
+}
+
+// get /ldap/users
+func (c *UserService) GetDetail(id int) (*models.LdapUser, error) {
+
+	o := orm.NewOrm()
+	user := models.LdapUser{Id: id}
+	err := o.Read(&user, "Id")
+	if err != nil {
+		return nil, err
+	}
+	user.Password = config.ReplacePassword
+	return &user, nil
+}