浏览代码

提交登录页面

fuwencai 4 年之前
父节点
当前提交
b7ced6444f
共有 2 个文件被更改,包括 501 次插入121 次删除
  1. 266 33
      src/front/front.go
  2. 235 88
      src/web/templates/login.html

+ 266 - 33
src/front/front.go

@@ -1,15 +1,20 @@
 package front
 
 import (
+	"fmt"
+	"github.com/dchest/captcha"
+	"github.com/go-xweb/xweb"
+	"github.com/gorilla/sessions"
+	"gopkg.in/mgo.v2/bson"
 	"log"
 	mongoutil "qfw/mongodb"
 	qu "qfw/util"
+	"qfw/util/sms"
+	"regexp"
 	"strconv"
+	"strings"
 	"time"
 	. "util"
-
-	"github.com/go-xweb/xweb"
-	"gopkg.in/mgo.v2/bson"
 )
 
 var UserMenu map[string][]map[string]interface{} //存储菜单
@@ -19,7 +24,10 @@ func init() {
 
 type Front struct {
 	*xweb.Action
-	login     xweb.Mapper `xweb:"/"`                //登录页面
+	login          xweb.Mapper `xweb:"/"`               //登录页面
+	code           xweb.Mapper `xweb:"/code"`           //获取图片验证码
+	sendVerifyCode xweb.Mapper `xweb:"/sendVerifyCode"` //获取手机验证码
+
 	index     xweb.Mapper `xweb:"/front/index"`     //index页面
 	logout    xweb.Mapper `xweb:"/front/logout"`    //注销
 	updatePwd xweb.Mapper `xweb:"/front/updatepwd"` //更新密码
@@ -46,43 +54,125 @@ type Front struct {
 	roleSecondEdit xweb.Mapper `xweb:"/front/role/second/edit"` //二级权限编辑
 }
 
+var store = sessions.NewCookieStore([]byte("jianyu_secret20210422"))
+
 func (f *Front) Login() {
 	defer qu.Catch()
 	if f.Method() == "POST" {
-		email := f.GetString("email")
-		password := f.GetString("pwd")
-		//f.SetSession("password", password)
-		passwordEn := qu.SE.EncodeString(password)
+		// 1. 验证参数有效性
+		phone := f.GetString("phone")
+		phoneCode := f.GetString("phoneCode")
+		reg := regexp.MustCompile("^1([3456789])\\d{9}$")
+		log.Println(phone)
+		log.Println(phoneCode)
+		if !reg.MatchString(phone) {
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "手机号格式有误",
+			})
+			return
+		}
+		if phoneCode == "" {
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "短信验证码不能为空",
+			})
+			return
+		}
+		// 2. 验证短信验证码
+		session, err := store.Get(f.Request, "dataTagLoginPhoneCode")
+		if err != nil {
+			log.Println("phone-session2获取失败")
+			return
+		}
+		realPhoneCode := qu.ObjToString(session.Values["code"])
+		log.Println("realPhoneCode", realPhoneCode)
+		log.Println("param.phoneCode", phoneCode)
+		sessionPhone := qu.ObjToString(session.Values["phone"])
+		if sessionPhone == "" {
+			log.Printf("短信验证码过期-%s \n", phone)
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "短信验证码过期",
+			})
+			return
+		}
+		if phoneCode != realPhoneCode || phone != sessionPhone {
+			log.Println("短信验证码错误")
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "短信验证码错误",
+			})
+			return
+		}
+		// 清理验证过的验证码
+		session.Values["code"] = ""
+		session.Values["phone"] = ""
+		if err := session.Save(f.Request, f.ResponseWriter); err != nil {
+			log.Println("session1清理出错,短信验证码")
+		}
+		log.Println("验证码验证通过")
+		// 3. 验证用户导出权限  确认认用户是否有数据导出权限
+		loginUser := JyMysql.SelectBySql("select id, name,ent_id,phone,export_power,name from entniche_user where phone=? and export_power=1", phone)
+		if len(*loginUser) == 0 || ((*loginUser)[0])["phone"] != phone {
+			log.Println("无权限")
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "无权限",
+			})
+			return
+		}
+		// 4. 根据用户的企业id查询对应的管理员手机号  根据管理员手机号查询jyqyfw的企业表对应的appid 放到session里  以及确认用户角色
+		log.Println((*loginUser)[0]["ent_id"], "=====================")
+		entInfo := JyMysql.SelectBySql("select phone,name from entniche_info WHERE id = ?", (*loginUser)[0]["ent_id"])
+		if len(*entInfo) == 0 {
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "企业信息查询失败",
+			})
+			return
+		}
+		var role int // 角色  1 管理员  3 普通用户
+		// 判断当前用户手机号是否是管理员手机号
+		if phone != qu.ObjToString((*entInfo)[0]["phone"]) {
+			role = 3
+		} else {
+			role = 1
+		}
+		// 查询mongo企业信息库 获取appid
 		query := bson.M{
-			"s_email": email,
-			"s_pwd":   passwordEn,
-		}
-		user, _ := Mgo.FindOne("user", query)
-		checked := false
-		s_id := ""
-		if user != nil && len(*user) > 0 {
-			checked = true
+			"phone":    qu.ObjToString((*entInfo)[0]["phone"]),
+			"username": qu.ObjToString((*entInfo)[0]["name"]),
+		}
+		entMgoInfo, ok := MgoCus.FindOne("user", query)
+		if !ok {
+			//	企业信息查询失败
+			return
+		}
+		//相关信息存入session
+		if entMgoInfo != nil && len(*entMgoInfo) > 0 {
 			f.SetSession("user", map[string]interface{}{
-				"name":  (*user)["s_name"],
-				"role":  (*user)["s_role"],
-				"pwd":   password,
-				"email": email,
-				"id":    mongoutil.BsonIdToSId((*user)["_id"]),
+				"name":   (*loginUser)[0]["name"],
+				"phone":  phone,
+				"role":   role,
+				"appid":  (*entMgoInfo)["appid"],
+				"ent_id": (*loginUser)[0]["entid"],
+				"id":     (*loginUser)[0]["id"],
 			})
-			UserMenu[email] = GetUserMenu(qu.ObjToString((*user)["s_role"]))
-			if (*user)["s_role"] == "3" {
-				log.Println("users", (*user)["s_name"])
-				users, ok := Mgo.FindOne("cuser", map[string]interface{}{"s_name": (*user)["s_name"], "b_delete": false})
-				log.Println("users", users)
-				if users != nil && ok {
-					s_id = mongoutil.BsonIdToSId((*users)["_id"])
-				}
-			}
+			//UserMenu[email] = GetUserMenu(qu.ObjToString((*user)["s_role"]))
+
 		}
 		f.ServeJson(map[string]interface{}{
-			"checked": checked,
-			"role":    (*user)["s_role"],
-			"id":      s_id,
+			"code":    0,
+			"status":  false,
+			"role":    role,
+			"message": "企业信息查询失败",
 		})
 	} else {
 		f.Render("login.html")
@@ -227,3 +317,146 @@ func GetUserMenu(role string) []map[string]interface{} {
 	}
 	return list
 }
+
+// 获取图片验证码
+func (f *Front) Code() {
+
+	id := captcha.NewLen(4)
+	//r := &http.Request{}
+	f.Request.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate")
+	f.Request.Header.Add("Pragma", "no-cache")
+	f.Request.Header.Add("Expires", "0")
+	f.Request.Header.Add("Content-Type", "image/png")
+	w := f.ResponseWriter
+	session, err := store.Get(f.Request, "dataTagLoginImgCode")
+	if err != nil {
+		log.Println("session1获取失败")
+		return
+	}
+	session.Values["dataTagLoginImgCode"] = id
+	session.Options.MaxAge = 60
+	if err := session.Save(f.Request, w); err != nil {
+		log.Println("session1保存错误,验证码 ", id)
+	}
+	err2 := captcha.WriteImage(w, id, 90, 30)
+	if err2 != nil {
+		log.Println("生成图片验证码错误,验证码 ", id)
+	}
+	return
+}
+
+// 发送手机验证码接口
+func (f *Front) SendVerifyCode() {
+	if f.Method() == "POST" {
+		//1. 验证参数有效性
+		phone := f.GetString("phone")
+		imgCode := f.GetString("imgCode")
+		reg := regexp.MustCompile("^1(3|4|5|6|7|8|9)\\d{9}$")
+		if !reg.MatchString(phone) {
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "手机号格式有误",
+			})
+			return
+		}
+		//2. 验证图片验证码
+		session, err := store.Get(f.Request, "dataTagLoginImgCode")
+		if err != nil {
+			log.Printf("图片验证码session获取失败-%s \n", phone)
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "获取失败",
+			})
+			return
+		}
+		code := qu.ObjToString(session.Values["dataTagLoginImgCode"])
+		if code == "" {
+			log.Printf("图片验证码过期-%s \n", phone)
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "图片验证码过期",
+			})
+			return
+		}
+		fmt.Println("code", code)
+		fmt.Println("img", imgCode)
+		if !captcha.VerifyString(code, imgCode) {
+			log.Printf("图片验证码错误-%s \n", phone)
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "图片验证码错误",
+			})
+			return
+		}
+		//3. 验证手机号是否存在
+		user := JyMysql.SelectBySql("select * from entniche_user where phone=? and export_power=1", phone)
+		fmt.Println(user, "-----------------================")
+		// 确认用户是否存在
+		if len(*user) == 0 || ((*user)[0])["phone"] != phone {
+			log.Println("没有数据导出权限")
+			f.ServeJson(map[string]interface{}{
+				"code":    0,
+				"status":  false,
+				"message": "无权限",
+			})
+			return
+		}
+		//4. 发送验证码
+		SendPhoneCode(f, phone)
+		f.ServeJson(map[string]interface{}{
+			"code":    0,
+			"status":  true,
+			"message": "发送成功",
+		})
+		return
+	} else {
+		f.ServeJson(map[string]interface{}{
+			"code":    0,
+			"status":  false,
+			"message": "无效的请求方式",
+		})
+	}
+
+}
+
+// 发送手机验证码方法
+func SendPhoneCode(f *Front, phone string) {
+	r := f.Request
+	w := f.ResponseWriter
+	session, err := store.Get(r, "dataTagLoginPhoneCode")
+	if err != nil {
+		log.Println("phone-session1获取失败")
+		return
+	}
+	lastSentTime := qu.Int64All(session.Values["identCodeTime"])
+	//60秒之内不允许重复发
+	if lastSentTime > 0 && time.Now().Unix()-lastSentTime <= 60 {
+	}
+	s_ranNum := qu.GetRandom(6) //生成随机数
+
+	session.Values["code"] = s_ranNum
+	session.Values["phone"] = phone
+	session.Values["identCodeTime"] = time.Now().Unix()
+	session.Options.MaxAge = 300
+	if err := session.Save(r, w); err != nil {
+		log.Println("session1保存错误,验证码")
+	}
+	//发送短信
+	//param := map[string]string{"code": s_ranNum}
+	log.Println("短信验证码", phone, s_ranNum)
+	//SendSMS("2828060", phone, param)
+}
+
+//第三个参数是可变参数,可以传入多个,但要和模板相匹配
+func SendSMS(tplcode /*模板代码*/, mobile /*手机号码*/ string, param map[string]string) {
+	tmp := []string{}
+	for k, v := range param {
+		tmp = append(tmp, "#"+k+"#="+v)
+	}
+	text := strings.Join(tmp, "&")
+	sms.SendSms(mobile, tplcode, text)
+}

+ 235 - 88
src/web/templates/login.html

@@ -1,60 +1,82 @@
 <!DOCTYPE html>
 <html>
 <head>
-  <meta charset="utf-8">
-  <meta http-equiv="X-UA-Compatible" content="IE=edge">
-  <title>Data Tags | Log in</title>
-  <!-- Tell the browser to be responsive to screen width -->
-  <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
-  <!-- Bootstrap 3.3.7 -->
-  <link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
-  <!-- Font Awesome -->
-  <link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.min.css">
-  <!-- Ionicons -->
-  <link rel="stylesheet" href="/bower_components/Ionicons/css/ionicons.min.css">
-  <!-- Theme style -->
-  <link rel="stylesheet" href="/dist/css/AdminLTE.min.css">
-  <!-- iCheck -->
-  <link rel="stylesheet" href="/plugins/iCheck/square/blue.css">
-
-  <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
-  <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
-  <!--[if lt IE 9]>
-  <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
-  <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
-  <![endif]-->
-
-  <!-- Google Font -->
-  <!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">--->
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>Data Tags | Log in</title>
+    <!-- Tell the browser to be responsive to screen width -->
+    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
+    <!-- Bootstrap 3.3.7 -->
+    <link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
+    <!-- Font Awesome -->
+    <link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.min.css">
+    <!-- Ionicons -->
+    <link rel="stylesheet" href="/bower_components/Ionicons/css/ionicons.min.css">
+    <!-- Theme style -->
+    <link rel="stylesheet" href="/dist/css/AdminLTE.min.css">
+    <!-- iCheck -->
+    <link rel="stylesheet" href="/plugins/iCheck/square/blue.css">
+
+    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
+    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+    <!--[if lt IE 9]>
+    <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
+    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+    <![endif]-->
+
+    <!-- Google Font -->
+    <!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">--->
 </head>
 <body class="hold-transition login-page">
 <div class="login-box">
-  <div class="login-logo">
-    <b>Data Tags</b>
-  </div>
-  <!-- /.login-logo -->
-  <div class="login-box-body">
-    <p class="login-box-msg">Sign in to start your session</p>
-
-    <form action="/login" method="post">
-      <div class="form-group has-feedback">
-        <input id="email" type="email" class="form-control" placeholder="请输入账号">
-        <span class="glyphicon glyphicon-envelope form-control-feedback"></span>
-      </div>
-      <div class="form-group has-feedback">
-        <input id="pwd" type="password" class="form-control" placeholder="请输入密码">
-        <span class="glyphicon glyphicon-lock form-control-feedback"></span>
-      </div>
-      <div class="row">
-        <!-- /.col -->
-        <div class="col-xs-12">
-          <button type="button" id="logina" class="btn btn-primary btn-block btn-flat" onclick="login()">登录</button>
-        </div>
-        <!-- /.col -->
-      </div>
-    </form>
-  </div>
-  <!-- /.login-box-body -->
+    <div class="login-logo">
+        <b>Data Tags</b>
+    </div>
+    <!-- /.login-logo -->
+    <div class="login-box-body">
+        <p class="login-box-msg">Sign in to start your session</p>
+
+        <form action="/" method="post" id="loginForm">
+            <div class="form-group has-feedback">
+                <input id="phone" name="phone" type="tel" class="form-control" placeholder="请输入手机号" maxlength="11"
+                       autocomplete="off">
+                <span class="glyphicon glyphicon-phone form-control-feedback"></span>
+            </div>
+            <div class="input-group form-group has-feedback " id="a" style="width: 100%">
+
+
+                <input type="text" class="form-control " style="float: left;z-index: 2;position: relative"
+                       placeholder="请输入图形验证码" maxlength="4"
+                       id="verifyImgCode">
+
+
+                <img border="0" style="cursor:pointer ;z-index: 5;position: absolute;right: 1px;width: 100px;top: 1px"
+                     alt="点击刷新验证码" src="/code"
+                     id="verifyImg">
+
+
+            </div>
+            <div class="input-group form-group has-feedback">
+                <input type="text" class="form-control " aria-describedby="sizing-addon1" placeholder="请输入短信验证码"
+                       maxlength="6" id="phoneCode"
+                       name="phoneCode">
+                <span class="input-group-btn ">
+        <button class="btn btn-default" type="button" id="sendVerifyCode">获取验证码</button>
+      </span>
+            </div>
+
+
+            <div class="row">
+                <!-- /.col -->
+                <div class="col-xs-12">
+                    <button type="button" onclick="login()" class="btn btn-primary btn-block btn-flat">登录
+                    </button>
+                </div>
+                <!-- /.col -->
+            </div>
+        </form>
+    </div>
+    <!-- /.login-box-body -->
 </div>
 <!-- /.login-box -->
 
@@ -65,45 +87,170 @@
 <!-- iCheck -->
 <script src="/plugins/iCheck/icheck.min.js"></script>
 <script>
-$(function () {
-  	$('input').iCheck({
-    	checkboxClass: 'icheckbox_square-blue',
-    	radioClass: 'iradio_square-blue',
-    	increaseArea: '20%' /* optional */
- 	});
-	
-	document.onkeydown = function (event) {
-        var e = event || window.event;
-        if (e && e.keyCode == 13) { //回车键的键值为13
-           login();
+    // 获取图片验证码
+    $("#verifyImg").click(function () {
+        console.log("lalala")
+        $(this)[0].src = '/code?' + Math.random()
+    })
+    // 获取数字验证码
+    $("#sendVerifyCode").click(function () {
+        console.log("lalala-发送验证码")
+        // 验证手机格式和图片验证码是否为空
+        if (checkPhone()) {
+            // 验证图片验证码位数
+            if ($("#verifyImgCode").val().trim().replace(/\s/g, "").length === 4) {
+                console.log("发送短信验证码")
+                // debugger
+                $.ajax({
+                        url: '/sendVerifyCode',
+                        data: {
+                            "phone": $("#phone").val(),
+                            "imgCode": $("#verifyImgCode").val()
+                        },
+                        method: "post",
+                        success: function (r) {
+                            if (r.code === 0) {
+                                if (r.status !== true) {
+                                    $("#verifyImg")[0].src = '/code?' + Math.random()
+                                    alert(r.message)
+                                } else {
+                                    addCookie("SecondNum", 60, 60);
+                                    console.log("倒计时")
+                                    countDown()
+                                    alert("发送成功")
+                                    debugger
+                                }
+                            } else {
+                                alert("无响应")
+                            }
+                        }
+                    }
+                )
+            } else {
+                $("#verifyImg")[0].src = '/code?' + Math.random()
+                // $("#a").addClass("has-error")
+                alert("图片验证码有误,请重填");
+
+            }
+
+
         }
-    }; 
-});
-	
-function login(){
-	var email = $("#email").val().trim().replace(/\s/g,"");
-	var pwd = $("#pwd").val().trim().replace(/\s/g,"");
-	if(email==""||pwd==""){
-		alert("账号和密码不能为空")
-		return false;
-	}
-	$.ajax({
-		url:"/",
-		type:"post",
-		data:{"email":email,"pwd":pwd},
-		success:function(r){
-			if(r.checked){
-                if(r.role == "3"){
-                    window.location.href="/client/index?id="+ r.id;
-                }else{
-                    window.location.href="/front/index";
+
+    })
+    // $(function () {
+    //     $('input').iCheck({
+    //         checkboxClass: 'icheckbox_square-blue',
+    //         radioClass: 'iradio_square-blue',
+    //         increaseArea: '20%' /* optional */
+    //     });
+    //
+    //     document.onkeydown = function (event) {
+    //         var e = event || window.event;
+    //         if (e && e.keyCode == 13) { //回车键的键值为13
+    //             login();
+    //         }
+    //     };
+    // });
+
+
+    function login() {
+        if (!checkPhone()) {
+            alert("手机号格式有误!")
+            return
+        }
+        // 短信验证码格式
+        if ($("#phoneCode").val().trim().replace(/\s/g, "").length !== 6) {
+            alert("短信验证码格式有误!")
+            return
+        }
+        $.ajax({
+            url: "/",
+            type: "post",
+            data: {
+                "phone": $("#phone").val(),
+                "phoneCode": $("#phoneCode").val()
+            },
+            success: function (r) {
+                if (r.status === false) {
+                    alert(r.message)
+                } else {
+                    if (r.role === "1") {
+                        window.location.href = "/front/index";
+                    } else {
+                        window.location.href = "/client/index";
+                    }
                 }
-			}else{
-				alert("fail");
-			}
-		}
-	})
-}
+            }
+        })
+    }
+
+    function checkPhone() {
+        var phone = document.getElementById('phone').value;
+        if (!(/^1(3|4|5|6|7|8|9)\d{9}$/.test(phone))) {
+            alert("手机号码有误,请重填");
+            return false;
+        } else return true
+    }
+
+    //倒计时
+    function countDown() {
+        debugger
+        console.log("倒计时")
+        var second = getCookieValue("SecondNum") ? getCookieValue("SecondNum") : 0;//获取cookie值
+        if (second == 0) {
+            $("#sendVerifyCode").attr("disabled", false);
+            document.getElementById("sendVerifyCode").style.cursor = "pointer";//启用鼠标
+            $("#sendVerifyCode").val('获取验证码');
+            addCookie("SecondNum", 60, 60); //添加cookie记录,有效时间60s
+            return;
+        } else {
+            $("#sendVerifyCode").attr("disabled", true);
+            document.getElementById("sendVerifyCode").style.cursor = "not-allowed";//禁用鼠标
+            $("#sendVerifyCode").text("重新获取(" + second + "s)");
+            second--;
+            editCookie("SecondNum", second, second + 1);
+        }
+
+        setTimeout(function () {
+            countDown();
+        }, 1000);
+    }
+
+    //发送验证码时添加cookie
+    function addCookie(name, value, expiresHours) {
+        var cookieString = name + "=" + escape(value);
+        //判断是否设置过期时间,0代表关闭浏览器时失效
+        if (expiresHours > 0) {
+            var date = new Date();
+            date.setTime(date.getTime() + expiresHours * 1000);
+            cookieString = cookieString + ";expires=" + date.toUTCString();
+        }
+        document.cookie = cookieString;
+    }
+
+    //修改cookie的值
+    function editCookie(name, value, expiresHours) {
+        var cookieString = name + "=" + escape(value);
+        if (expiresHours > 0) {
+            var date = new Date();
+            date.setTime(date.getTime() + expiresHours * 1000); //单位是毫秒
+            cookieString = cookieString + ";expires=" + date.toGMTString();
+        }
+        document.cookie = cookieString;
+    }
+
+    //根据名字获取cookie的值
+    function getCookieValue(name) {
+        var strCookie = document.cookie;
+        var arrCookie = strCookie.split("; ");
+        for (var i = 0; i < arrCookie.length; i++) {
+            var arr = arrCookie[i].split("=");
+            if (arr[0] == name) {
+                return unescape(arr[1]);
+                break;
+            }
+        }
+    }
 
 </script>
 </body>