| @@ -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! | |||
| @@ -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>! | |||
| @@ -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. | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -53,6 +53,7 @@ | |||
| #repo-hooks-panel, | |||
| #repo-hooks-history-panel, | |||
| #user-social-panel, | |||
| #user-ssh-panel { | |||
| margin-bottom: 20px; | |||
| .setting-list { | |||
| @@ -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") | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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> | |||
| @@ -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" .}} | |||