diff options
author | Unknwon <joe2010xtmf@163.com> | 2014-08-09 17:25:02 -0700 |
---|---|---|
committer | Unknwon <joe2010xtmf@163.com> | 2014-08-09 17:25:02 -0700 |
commit | 78defd238c939ff577041f2e7b95b2ae48a9fb27 (patch) | |
tree | 5dd168d880ece655796558874f352b2c2263e965 | |
parent | 08c6d07aad65f45efd5bf9f50d9cda68f59c0e69 (diff) | |
download | gitea-78defd238c939ff577041f2e7b95b2ae48a9fb27.tar.gz gitea-78defd238c939ff577041f2e7b95b2ae48a9fb27.zip |
Page: Manage social accounts
-rw-r--r-- | conf/locale/locale_en-US.ini | 8 | ||||
-rw-r--r-- | conf/locale/locale_zh-CN.ini | 7 | ||||
-rw-r--r-- | models/oauth2.go | 36 | ||||
-rw-r--r-- | modules/middleware/repo.go | 6 | ||||
-rw-r--r-- | public/ng/css/gogs.css | 11 | ||||
-rw-r--r-- | public/ng/less/gogs/settings.less | 1 | ||||
-rw-r--r-- | routers/user/auth.go | 79 | ||||
-rw-r--r-- | routers/user/setting.go | 45 | ||||
-rw-r--r-- | routers/user/social.go | 10 | ||||
-rw-r--r-- | templates/repo/settings/options.tmpl | 2 | ||||
-rw-r--r-- | templates/user/settings/social.tmpl | 19 | ||||
-rw-r--r-- | templates/user/signin.tmpl | 8 | ||||
-rw-r--r-- | templates/user/signup.tmpl | 4 | ||||
-rw-r--r-- | templates/user/social.tmpl | 37 |
14 files changed, 149 insertions, 124 deletions
diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini index 42fffa08da..9e2302195d 100644 --- a/conf/locale/locale_en-US.ini +++ b/conf/locale/locale_en-US.ini @@ -5,6 +5,7 @@ dashboard = Dashboard explore = Explore help = Help sign_in = Sign In +social_sign_in = Social Sign In: 2nd Step <small>associate account</small> sign_out = Sign Out sign_up = Sign Up register = Register @@ -49,6 +50,7 @@ my_mirrors = My Mirrors [auth] create_new_account = Create New Account register_hepler_msg = Already have an account? Sign in now! +social_register_hepler_msg = Already have an account? Bind now! disable_register_prompt = Sorry, registration has been disabled. Please contact the site administrator. remember_me = Remember Me forget_password = Fotget password? @@ -129,8 +131,12 @@ add_on = Added on last_used = Last used on no_activity = No recent activity -manage_orgs = Manage Organizations manage_social = Manage Associated Social Accounts +social_desc = This is a list of associated social accounts. Remove any binding that you do not recognize. +unbind = Unbind +unbind_success = Social account has been unbinded. + +manage_orgs = Manage Organizations delete_account = Delete Your Account delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undo! diff --git a/conf/locale/locale_zh-CN.ini b/conf/locale/locale_zh-CN.ini index fb49ade8c4..31b99c6024 100644 --- a/conf/locale/locale_zh-CN.ini +++ b/conf/locale/locale_zh-CN.ini @@ -49,6 +49,7 @@ my_mirrors = 我的镜像 [auth] create_new_account = 创建帐户 register_hepler_msg = 已经注册?立即登录! +social_register_hepler_msg = 已经注册?立即绑定! disable_register_prompt = 对不起,注册功能已被关闭。请联系网站管理员。 remember_me = 记住登录 forget_password = 忘记密码? @@ -129,8 +130,12 @@ add_on = 增加于 last_used = 上次使用在 no_activity = 没有最近活动 -manage_orgs = 管理我的组织 manage_social = 管理关联社交帐户 +social_desc = 以下是与您帐户所关联的社交帐号,如果您发现有陌生的关联,请立即解除绑定! +unbind = 解除绑定 +unbind_success = 社交帐号解除绑定成功! + +manage_orgs = 管理我的组织 delete_account = 删除当前帐户 delete_prompt = 删除操作会永久清除您的帐户信息,并且 <strong>不可恢复</strong>! diff --git a/models/oauth2.go b/models/oauth2.go index 4b024a26e4..46e8e492a3 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -6,6 +6,7 @@ package models import ( "errors" + "time" ) type OauthType int @@ -26,12 +27,15 @@ var ( ) type Oauth2 struct { - Id int64 - Uid int64 `xorm:"unique(s)"` // userId - User *User `xorm:"-"` - Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... - Identity string `xorm:"unique(s) unique(oauth)"` // id.. - Token string `xorm:"TEXT not null"` + Id int64 + Uid int64 `xorm:"unique(s)"` // userId + User *User `xorm:"-"` + Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... + Identity string `xorm:"unique(s) unique(oauth)"` // id.. + Token string `xorm:"TEXT not null"` + Created time.Time `xorm:"CREATED"` + Updated time.Time + HasRecentActivity bool `xorm:"-"` } func BindUserOauth2(userId, oauthId int64) error { @@ -69,10 +73,24 @@ func GetOauth2ById(id int64) (oa *Oauth2, err error) { return oa, nil } +// UpdateOauth2 updates given OAuth2. +func UpdateOauth2(oa *Oauth2) error { + _, err := x.Id(oa.Id).AllCols().Update(oa) + return err +} + // GetOauthByUserId returns list of oauthes that are releated to given user. -func GetOauthByUserId(uid int64) (oas []*Oauth2, err error) { - err = x.Find(&oas, Oauth2{Uid: uid}) - return oas, err +func GetOauthByUserId(uid int64) ([]*Oauth2, error) { + socials := make([]*Oauth2, 0, 5) + err := x.Find(&socials, Oauth2{Uid: uid}) + if err != nil { + return nil, err + } + + for _, social := range socials { + social.HasRecentActivity = social.Updated.Add(7 * 24 * time.Hour).After(time.Now()) + } + return socials, err } // DeleteOauth2ById deletes a oauth2 by ID. diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index 16bd7defb8..e1c5c68c75 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -253,7 +253,10 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { } if ctx.IsSigned { - ctx.Repo.IsWatching = models.IsWatching(ctx.User.Id, repo.Id) + ctx.Data["IsWatchingRepo"] = models.IsWatching(ctx.User.Id, repo.Id) + } + if ctx.Repo.Repository.IsBare { + return } ctx.Data["TagName"] = ctx.Repo.TagName @@ -276,7 +279,6 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Data["BranchName"] = ctx.Repo.BranchName ctx.Data["CommitId"] = ctx.Repo.CommitId - ctx.Data["IsWatchingRepo"] = ctx.Repo.IsWatching } } diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css index 0c3a40db38..9633ed2791 100644 --- a/public/ng/css/gogs.css +++ b/public/ng/css/gogs.css @@ -1365,32 +1365,38 @@ The register and sign-in page style } #repo-hooks-panel, #repo-hooks-history-panel, +#user-social-panel, #user-ssh-panel { margin-bottom: 20px; } #repo-hooks-panel .setting-list, #repo-hooks-history-panel .setting-list, +#user-social-panel .setting-list, #user-ssh-panel .setting-list { background-color: #FFF; } #repo-hooks-panel .setting-list li, #repo-hooks-history-panel .setting-list li, +#user-social-panel .setting-list li, #user-ssh-panel .setting-list li { padding: 8px 20px; border-bottom: 1px solid #eaeaea; } #repo-hooks-panel .setting-list li.ssh:hover, #repo-hooks-history-panel .setting-list li.ssh:hover, +#user-social-panel .setting-list li.ssh:hover, #user-ssh-panel .setting-list li.ssh:hover { background-color: #ffffEE; } #repo-hooks-panel .setting-list li i, #repo-hooks-history-panel .setting-list li i, +#user-social-panel .setting-list li i, #user-ssh-panel .setting-list li i { padding-right: 5px; } #repo-hooks-panel .active-icon, #repo-hooks-history-panel .active-icon, +#user-social-panel .active-icon, #user-ssh-panel .active-icon { width: 10px; height: 10px; @@ -1401,24 +1407,29 @@ The register and sign-in page style } #repo-hooks-panel .ssh-content, #repo-hooks-history-panel .ssh-content, +#user-social-panel .ssh-content, #user-ssh-panel .ssh-content { margin-left: 24px; } #repo-hooks-panel .ssh-content .octicon, #repo-hooks-history-panel .ssh-content .octicon, +#user-social-panel .ssh-content .octicon, #user-ssh-panel .ssh-content .octicon { margin-right: 4px; } #repo-hooks-panel .ssh-content .print, #repo-hooks-history-panel .ssh-content .print, +#user-social-panel .ssh-content .print, #user-ssh-panel .ssh-content .print, #repo-hooks-panel .ssh-content .activity, #repo-hooks-history-panel .ssh-content .activity, +#user-social-panel .ssh-content .activity, #user-ssh-panel .ssh-content .activity { color: #888; } #repo-hooks-panel .ssh-delete-btn, #repo-hooks-history-panel .ssh-delete-btn, +#user-social-panel .ssh-delete-btn, #user-ssh-panel .ssh-delete-btn { margin-top: 6px; } diff --git a/public/ng/less/gogs/settings.less b/public/ng/less/gogs/settings.less index af38ca28f5..1a492b03c2 100644 --- a/public/ng/less/gogs/settings.less +++ b/public/ng/less/gogs/settings.less @@ -53,6 +53,7 @@ #repo-hooks-panel, #repo-hooks-history-panel, +#user-social-panel, #user-ssh-panel { margin-bottom: 20px; .setting-list { diff --git a/routers/user/auth.go b/routers/user/auth.go index 710d048f39..191da0a219 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -14,7 +14,7 @@ import ( "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" - // "github.com/gogits/gogs/modules/mailer" + "github.com/gogits/gogs/modules/mailer" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/setting" ) @@ -157,23 +157,22 @@ func SignOut(ctx *middleware.Context) { } func oauthSignUp(ctx *middleware.Context, sid int64) { - // ctx.Data["Title"] = "OAuth Sign Up" - // ctx.Data["PageIsSignUp"] = true + ctx.Data["Title"] = ctx.Tr("sign_up") - // if _, err := models.GetOauth2ById(sid); err != nil { - // if err == models.ErrOauth2RecordNotExist { - // ctx.Handle(404, "user.oauthSignUp(GetOauth2ById)", err) - // } else { - // ctx.Handle(500, "user.oauthSignUp(GetOauth2ById)", err) - // } - // return - // } + if _, err := models.GetOauth2ById(sid); err != nil { + if err == models.ErrOauth2RecordNotExist { + ctx.Handle(404, "GetOauth2ById", err) + } else { + ctx.Handle(500, "GetOauth2ById", err) + } + return + } - // ctx.Data["IsSocialLogin"] = true - // ctx.Data["username"] = strings.Replace(ctx.Session.Get("socialName").(string), " ", "", -1) - // ctx.Data["email"] = ctx.Session.Get("socialEmail") - // log.Trace("user.oauthSignUp(social ID): %v", ctx.Session.Get("socialId")) - // ctx.HTML(200, SIGNUP) + ctx.Data["IsSocialLogin"] = true + ctx.Data["uname"] = strings.Replace(ctx.Session.Get("socialName").(string), " ", "", -1) + ctx.Data["email"] = ctx.Session.Get("socialEmail") + log.Trace("social ID: %v", ctx.Session.Get("socialId")) + ctx.HTML(200, SIGNUP) } func SignUp(ctx *middleware.Context) { @@ -202,10 +201,10 @@ func SignUpPost(ctx *middleware.Context, cpt *captcha.Captcha, form auth.Registe } isOauth := false - // sid, isOauth := ctx.Session.Get("socialId").(int64) - // if isOauth { - // ctx.Data["IsSocialLogin"] = true - // } + sid, isOauth := ctx.Session.Get("socialId").(int64) + if isOauth { + ctx.Data["IsSocialLogin"] = true + } // May redirect from home page. if ctx.Query("from") == "home" { @@ -268,28 +267,28 @@ func SignUpPost(ctx *middleware.Context, cpt *captcha.Captcha, form auth.Registe log.Trace("Account created: %s", u.Name) // Bind social account. - // if isOauth { - // if err = models.BindUserOauth2(u.Id, sid); err != nil { - // ctx.Handle(500, "user.SignUp(BindUserOauth2)", err) - // return - // } - // ctx.Session.Delete("socialId") - // log.Trace("%s OAuth binded: %s -> %d", ctx.Req.RequestURI, form.UserName, sid) - // } + if isOauth { + if err := models.BindUserOauth2(u.Id, sid); err != nil { + ctx.Handle(500, "BindUserOauth2", err) + return + } + ctx.Session.Delete("socialId") + log.Trace("%s OAuth binded: %s -> %d", ctx.Req.RequestURI, form.UserName, sid) + } // Send confirmation e-mail, no need for social account. - // if !isOauth && setting.Service.RegisterEmailConfirm && u.Id > 1 { - // mailer.SendRegisterMail(ctx.Render, u) - // ctx.Data["IsSendRegisterMail"] = true - // ctx.Data["Email"] = u.Email - // ctx.Data["Hours"] = setting.Service.ActiveCodeLives / 60 - // ctx.HTML(200, "user/activate") - - // if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { - // log.Error("Set cache(MailResendLimit) fail: %v", err) - // } - // return - // } + if !isOauth && setting.Service.RegisterEmailConfirm && u.Id > 1 { + mailer.SendRegisterMail(ctx.Render, u) + ctx.Data["IsSendRegisterMail"] = true + ctx.Data["Email"] = u.Email + ctx.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + ctx.HTML(200, "user/activate") + + if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + return + } ctx.Redirect("/user/login") } diff --git a/routers/user/setting.go b/routers/user/setting.go index 761052144f..739a30d032 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -200,36 +200,29 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) { ctx.HTML(200, SETTINGS_SSH_KEYS) } -// func SettingSocial(ctx *middleware.Context) { -// ctx.Data["Title"] = "Social Account" -// ctx.Data["PageIsUserSetting"] = true -// ctx.Data["IsUserPageSettingSocial"] = true - -// // Unbind social account. -// remove, _ := base.StrTo(ctx.Query("remove")).Int64() -// if remove > 0 { -// if err := models.DeleteOauth2ById(remove); err != nil { -// ctx.Handle(500, "user.SettingSocial(DeleteOauth2ById)", err) -// return -// } -// ctx.Flash.Success("OAuth2 has been unbinded.") -// ctx.Redirect("/user/settings/social") -// return -// } - -// var err error -// ctx.Data["Socials"], err = models.GetOauthByUserId(ctx.User.Id) -// if err != nil { -// ctx.Handle(500, "user.SettingSocial(GetOauthByUserId)", err) -// return -// } -// ctx.HTML(200, SOCIAL) -// } - func SettingsSocial(ctx *middleware.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsUserSettings"] = true ctx.Data["PageIsSettingsSocial"] = true + + // Unbind social account. + remove, _ := com.StrTo(ctx.Query("remove")).Int64() + if remove > 0 { + if err := models.DeleteOauth2ById(remove); err != nil { + ctx.Handle(500, "DeleteOauth2ById", err) + return + } + ctx.Flash.Success(ctx.Tr("settings.unbind_success")) + ctx.Redirect("/user/settings/social") + return + } + + socials, err := models.GetOauthByUserId(ctx.User.Id) + if err != nil { + ctx.Handle(500, "GetOauthByUserId", err) + return + } + ctx.Data["Socials"] = socials ctx.HTML(200, SETTINGS_SOCIAL) } diff --git a/routers/user/social.go b/routers/user/social.go index ef83cd5b42..d7486dad2b 100644 --- a/routers/user/social.go +++ b/routers/user/social.go @@ -10,6 +10,7 @@ import ( "fmt" "net/url" "strings" + "time" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/log" @@ -67,8 +68,8 @@ func SocialSignIn(ctx *middleware.Context) { oa, err := models.GetOauth2(ui.Identity) switch err { case nil: - ctx.Session.Set("userId", oa.User.Id) - ctx.Session.Set("userName", oa.User.Name) + ctx.Session.Set("uid", oa.User.Id) + ctx.Session.Set("uname", oa.User.Name) case models.ErrOauth2RecordNotExist: raw, _ := json.Marshal(tk) oa = &models.Oauth2{ @@ -89,6 +90,11 @@ func SocialSignIn(ctx *middleware.Context) { return } + oa.Updated = time.Now() + if err = models.UpdateOauth2(oa); err != nil { + log.Error(4, "UpdateOauth2: %v", err) + } + ctx.Session.Set("socialId", oa.Id) ctx.Session.Set("socialName", ui.Name) ctx.Session.Set("socialEmail", ui.Email) diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index e6ebb9aa22..4cc67c9d57 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -30,6 +30,7 @@ </div> <hr> <br> + {{if not .Repository.IsBare}} <div class="field"> <label>{{.i18n.Tr "repo.default_branch"}}</label> <select name="branch"> @@ -39,6 +40,7 @@ {{end}} </select> </div> + {{end}} {{if .Repository.IsMirror}} <div class="field"> <label for="interval">{{.i18n.Tr "repo.mirror_interval"}}</label> diff --git a/templates/user/settings/social.tmpl b/templates/user/settings/social.tmpl index 7ff2ea237f..bcbc1fc567 100644 --- a/templates/user/settings/social.tmpl +++ b/templates/user/settings/social.tmpl @@ -7,8 +7,23 @@ <div class="setting-content"> {{template "ng/base/alert" .}} <div id="setting-content"> - <div id="user-profile-setting-content" class="panel panel-radius"> - <p class="panel-header"><strong>{{.i18n.Tr "settings.manage_social"}}</strong></p> + <div id="user-social-panel" class="panel panel-radius"> + <div class="panel-header"><strong>{{.i18n.Tr "settings.manage_social"}}</strong></div> + <ul class="panel-body setting-list"> + <li>{{.i18n.Tr "settings.social_desc"}}</li> + {{range .Socials}} + <li class="ssh clear"> + <span class="active-icon left label label-{{if .HasRecentActivity}}green{{else}}gray{{end}} label-radius"></span> + <i class="fa {{Oauth2Icon .Type}} fa-2x left"></i> + <div class="ssh-content left"> + <p><strong>{{Oauth2Name .Type}}</strong></p> + <p class="print">{{.Identity}}</p> + <p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} {{DateFormat .Created "M d, Y"}} — <i class="octicon octicon-info"></i>{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}</i></p> + </div> + <a class="right btn btn-small btn-red btn-header btn-radius" href="/user/settings/social?remove={{.Id}}">{{$.i18n.Tr "settings.unbind"}}</a> + </li> + {{end}} + </ul> </div> </div> </div> diff --git a/templates/user/signin.tmpl b/templates/user/signin.tmpl index 9db98cd008..bc5c0c0c86 100644 --- a/templates/user/signin.tmpl +++ b/templates/user/signin.tmpl @@ -3,7 +3,7 @@ <div id="sign-wrapper"> <form class="form-align form panel sign-panel sign-form container panel-radius" id="sign-up-form" action="/user/login" method="post"> <div class="panel-header"> - <h2>{{.i18n.Tr "sign_in"}}</h2> + <h2>{{if .IsSocialLogin}}{{.i18n.Tr "social_sign_in" | Str2html}}{{else}}{{.i18n.Tr "sign_in"}}{{end}}</h2> </div> <div class="panel-content"> {{template "ng/base/alert" .}} @@ -15,15 +15,18 @@ <label class="req" for="password">{{.i18n.Tr "password"}}</label> <input class="ipt ipt-large ipt-radius {{if .Err_Password}}ipt-error{{end}}" id="password" name="password" type="password" required/> </p> + {{if not .IsSocialLogin}} <p class="field"> <span class="form-label"></span> <input class="ipt-chk" id="remember" name="remember" type="checkbox"/> <strong>{{.i18n.Tr "auth.remember_me"}}</strong> </p> + {{end}} <p class="field"> <span class="form-label"></span> <button class="btn btn-green btn-large btn-radius">{{.i18n.Tr "sign_in"}}</button> - <a href="/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a> + {{if not .IsSocialLogin}}<a href="/user/forget_password">{{.i18n.Tr "auth.forget_password"}}</a>{{end}} </p> + {{if not .IsSocialLogin}} <p class="field"> <span class="form-label"></span> <a href="/user/sign_up">{{.i18n.Tr "auth.sign_up_now" | Str2html}}</a> @@ -34,6 +37,7 @@ {{template "ng/base/social" .}} </div> {{end}} + {{end}} </div> </form> </div> diff --git a/templates/user/signup.tmpl b/templates/user/signup.tmpl index 723314956c..8d4a572ef5 100644 --- a/templates/user/signup.tmpl +++ b/templates/user/signup.tmpl @@ -3,7 +3,7 @@ <div id="sign-wrapper"> <form class="form-align form panel panel-radius sign-panel sign-form container" id="sign-up-form" action="/user/sign_up" method="post"> <div class="panel-header"> - <h2>{{.i18n.Tr "sign_up"}}</h2> + <h2>{{if .IsSocialLogin}}{{.i18n.Tr "social_sign_in" | Str2html}}{{else}}{{.i18n.Tr "sign_up"}}{{end}}</h2> </div> <div class="panel-content"> {{template "ng/base/alert" .}} @@ -40,7 +40,7 @@ </p> <p class="field"> <span class="form-label"></span> - <a href="/user/login">{{.i18n.Tr "auth.register_hepler_msg"}}</a> + <a href="/user/login">{{if .IsSocialLogin}}{{.i18n.Tr "auth.social_register_hepler_msg"}}{{else}}{{.i18n.Tr "auth.register_hepler_msg"}}{{end}}</a> </p> {{end}} </div> diff --git a/templates/user/social.tmpl b/templates/user/social.tmpl deleted file mode 100644 index 7814cc0956..0000000000 --- a/templates/user/social.tmpl +++ /dev/null @@ -1,37 +0,0 @@ -{{template "base/head" .}} -{{template "base/navbar" .}} -<div id="body" class="container" data-page="user"> - {{template "user/setting_nav" .}} - <div id="repo-setting-container" class="col-md-10"> - {{template "base/alert" .}} - <div class="panel panel-default"> - <div class="panel-heading"> - Social Account - </div> - - <div class="panel-body"> - <table class="table"> - <thead> - <tr> - <th></th> - <th>Name</th> - <th>Identity</th> - <th>Op.</th> - </tr> - </thead> - <tbody> - {{range .Socials}} - <tr> - <td><i class="fa {{Oauth2Icon .Type}} fa-2x"></i></td> - <td>{{Oauth2Name .Type}}</td> - <td>{{.Identity}}</td> - <td><a href="/user/settings/social?remove={{.Id}}">Unbind</a></td> - </tr> - {{end}} - </tbody> - </table> - </div> - </div> - </div> -</div> -{{template "base/footer" .}}
\ No newline at end of file |