Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package emailverify
import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"net/mail"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
// 是否可以激活Email
registerConfig, _ := configloaders.LoadRegisterConfig()
this.Data["canVerify"] = false
if registerConfig == nil {
this.Show()
return
}
var canVerify = registerConfig.EmailVerification.IsOn
if !canVerify {
this.Show()
return
}
this.Data["canVerify"] = true
// 当前已激活邮箱
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{
UserId: this.UserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
if userResp.User == nil {
this.ErrorPage(errors.New("can not find user info"))
return
}
this.Data["verifiedEmail"] = userResp.User.VerifiedEmail
// 现在正等待激活的邮箱
latestVerificationResp, err := this.RPC().UserEmailVerificationRPC().FindLatestUserEmailVerification(this.UserContext(), &pb.FindLatestUserEmailVerificationRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var latestVerification = latestVerificationResp.UserEmailVerification
if latestVerification == nil {
this.Data["latestVerification"] = nil
} else {
this.Data["latestVerification"] = maps.Map{
"email": latestVerification.Email,
"expiresTime": timeutil.FormatTime("Y-m-d H:i:s", latestVerification.ExpiresAt),
"isSent": latestVerification.IsSent,
}
}
this.Show()
}
func (this *IndexAction) RunPost(params struct {
Email string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
if len(params.Email) == 0 {
this.FailField("email", "请输入要激活的邮箱")
return
}
_, err := mail.ParseAddress(params.Email)
if err != nil {
this.FailField("email", "邮箱格式不正确")
return
}
// 检查邮件是否已被使用
checkResp, err := this.RPC().UserRPC().CheckUserEmail(this.UserContext(), &pb.CheckUserEmailRequest{Email: params.Email})
if err != nil {
this.ErrorPage(err)
return
}
if checkResp.Exists {
this.FailField("email", "当前邮箱已被别的用户使用,请换一个")
return
}
// 如果同自己已认证邮箱一致,则不重复发送
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
if userResp.User == nil {
this.Fail("can not find current user")
return
}
if userResp.User.VerifiedEmail == params.Email {
this.FailField("email", "此邮箱已激活,无需再次激活")
return
}
// 发送激活邮件
_, err = this.RPC().UserEmailVerificationRPC().SendUserEmailVerification(this.UserContext(), &pb.SendUserEmailVerificationRequest{Email: params.Email})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,18 @@
package emailverify
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(settingutils.NewHelper("profile")).
Prefix("/settings/email-verify").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type CancelAction struct {
actionutils.ParentAction
}
func (this *CancelAction) RunPost(params struct {
IdentityId int64
}) {
defer this.CreateLogInfo(codes.UserIdentity_LogCancelUserIdentity)
_, err := this.RPC().UserIdentityRPC().CancelUserIdentity(this.UserContext(), &pb.CancelUserIdentityRequest{UserIdentityId: params.IdentityId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,51 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type EnterpriseAction struct {
actionutils.ParentAction
}
func (this *EnterpriseAction) Init() {
this.Nav("", "", "")
}
func (this *EnterpriseAction) RunGet(params struct{}) {
resp, err := this.RPC().UserIdentityRPC().FindEnabledUserIdentityWithOrgType(this.UserContext(), &pb.FindEnabledUserIdentityWithOrgTypeRequest{
OrgType: userconfigs.UserIdentityOrgTypeEnterprise,
})
if err != nil {
this.ErrorPage(err)
return
}
var userIdentity = resp.UserIdentity
if userIdentity == nil {
this.Data["identity"] = nil
this.Show()
return
}
if len(userIdentity.FileIds) != 1 {
this.Data["identity"] = nil
this.Show()
return
}
this.Data["identity"] = maps.Map{
"id": userIdentity.Id,
"realName": userIdentity.RealName,
"number": userIdentity.Number,
"status": userIdentity.Status,
"frontFileId": userIdentity.FileIds[0],
"rejectReason": userIdentity.RejectReason,
}
this.Show()
}

View File

@@ -0,0 +1,62 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
// 个人认证
{
resp, err := this.RPC().UserIdentityRPC().FindEnabledUserIdentityWithOrgType(this.UserContext(), &pb.FindEnabledUserIdentityWithOrgTypeRequest{
UserId: this.UserId(),
OrgType: userconfigs.UserIdentityOrgTypeIndividual,
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.UserIdentity == nil {
this.Data["individualIdentity"] = nil
} else {
this.Data["individualIdentity"] = maps.Map{
"realName": resp.UserIdentity.RealName,
"status": resp.UserIdentity.Status,
}
}
}
// 企业认证
{
resp, err := this.RPC().UserIdentityRPC().FindEnabledUserIdentityWithOrgType(this.UserContext(), &pb.FindEnabledUserIdentityWithOrgTypeRequest{
UserId: this.UserId(),
OrgType: userconfigs.UserIdentityOrgTypeEnterprise,
})
if err != nil {
this.ErrorPage(err)
return
}
if resp.UserIdentity == nil {
this.Data["enterpriseIdentity"] = nil
} else {
this.Data["enterpriseIdentity"] = maps.Map{
"realName": resp.UserIdentity.RealName,
"status": resp.UserIdentity.Status,
}
}
}
this.Show()
}

View File

@@ -0,0 +1,52 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type IndividualAction struct {
actionutils.ParentAction
}
func (this *IndividualAction) Init() {
this.Nav("", "", "")
}
func (this *IndividualAction) RunGet(params struct{}) {
resp, err := this.RPC().UserIdentityRPC().FindEnabledUserIdentityWithOrgType(this.UserContext(), &pb.FindEnabledUserIdentityWithOrgTypeRequest{
OrgType: userconfigs.UserIdentityOrgTypeIndividual,
})
if err != nil {
this.ErrorPage(err)
return
}
var userIdentity = resp.UserIdentity
if userIdentity == nil {
this.Data["identity"] = nil
this.Show()
return
}
if len(userIdentity.FileIds) != 2 {
this.Data["identity"] = nil
this.Show()
return
}
this.Data["identity"] = maps.Map{
"id": userIdentity.Id,
"realName": userIdentity.RealName,
"number": userIdentity.Number,
"status": userIdentity.Status,
"frontFileId": userIdentity.FileIds[1],
"backFileId": userIdentity.FileIds[0],
"rejectReason": userIdentity.RejectReason,
}
this.Show()
}

View File

@@ -0,0 +1,24 @@
package identity
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(settingutils.NewHelper("identity")).
Prefix("/settings/identity").
Get("", new(IndexAction)).
Get("/individual", new(IndividualAction)).
Get("/enterprise", new(EnterpriseAction)).
Post("/uploadIndividual", new(UploadIndividualAction)).
Post("/uploadEnterprise", new(UploadEnterpriseAction)).
Post("/submit", new(SubmitAction)).
Post("/cancel", new(CancelAction)).
EndAll()
})
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type SubmitAction struct {
actionutils.ParentAction
}
func (this *SubmitAction) RunPost(params struct {
IdentityId int64
}) {
defer this.CreateLogInfo(codes.UserIdentity_LogSubmitUserIdentity)
_, err := this.RPC().UserIdentityRPC().SubmitUserIdentity(this.UserContext(), &pb.SubmitUserIdentityRequest{UserIdentityId: params.IdentityId})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,130 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/sizes"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"regexp"
"strings"
)
type UploadEnterpriseAction struct {
actionutils.ParentAction
}
func (this *UploadEnterpriseAction) RunPost(params struct {
IdentityId int64
RealName string
Number string
FrontFile *actions.File
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo(codes.UserIdentity_LogUpdateUserIdentityEnterprise)
// 检查IdentityId
var identityId = params.IdentityId
var oldFileIds = []int64{0, 0}
if identityId > 0 {
identityResp, err := this.RPC().UserIdentityRPC().FindEnabledUserIdentity(this.UserContext(), &pb.FindEnabledUserIdentityRequest{
UserIdentityId: identityId,
})
if err != nil {
this.ErrorPage(err)
return
}
if identityResp.UserIdentity == nil {
this.Fail("错误的表单数据,请刷新后重试")
}
var status = identityResp.UserIdentity.Status
if status != userconfigs.UserIdentityStatusNone && status != userconfigs.UserIdentityStatusRejected {
this.Fail("当前状态下无法修改")
}
oldFileIds = identityResp.UserIdentity.FileIds
}
var mustUpload = identityId <= 0
params.Must.
Field("realName", params.RealName).
Require("请输入企业全称").
Field("number", params.Number).
Require("请输入营业执照号码")
var number = regexp.MustCompile(`\s+`).ReplaceAllString(params.Number, "")
number = strings.ToUpper(number)
// front
var frontFile = params.FrontFile
if frontFile == nil {
if mustUpload {
this.Fail("请上传营业执照照片")
}
} else {
var frontContentType = strings.ToLower(frontFile.ContentType)
_, ok := allowedContentTypes[frontContentType]
if !ok {
this.Fail("请上传正确格式的图片")
}
// TODO 需要可以配置最大上传尺寸,包括界面上的提示
if frontFile.Size > 8*sizes.M {
this.Fail("要上传的图片营业执照照片不能超过8MiB")
}
}
// 上传到服务器
var frontFileId int64 = 0
if frontFile != nil {
fileId, ok, err := this.UploadFile(frontFile, "enterprise.license")
if err != nil {
this.ErrorPage(err)
return
}
if !ok {
this.Fail("上传失败,请刷新后重试")
}
frontFileId = fileId
}
if len(oldFileIds) == 1 {
if frontFileId == 0 {
frontFileId = oldFileIds[0]
}
}
var err error
if identityId > 0 {
_, err = this.RPC().UserIdentityRPC().UpdateUserIdentity(this.UserContext(), &pb.UpdateUserIdentityRequest{
UserIdentityId: identityId,
Type: userconfigs.UserIdentityTypeEnterpriseLicense,
RealName: params.RealName,
Number: number,
FileIds: []int64{frontFileId},
})
} else {
_, err = this.RPC().UserIdentityRPC().CreateUserIdentity(this.UserContext(), &pb.CreateUserIdentityRequest{
OrgType: userconfigs.UserIdentityOrgTypeEnterprise,
Type: userconfigs.UserIdentityTypeEnterpriseLicense,
RealName: params.RealName,
Number: number,
FileIds: []int64{frontFileId},
})
}
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,177 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package identity
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/utils/sizes"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"regexp"
"strings"
)
var allowedContentTypes = map[string]bool{
"image/png": true,
"image/jpeg": true,
}
type UploadIndividualAction struct {
actionutils.ParentAction
}
func (this *UploadIndividualAction) RunPost(params struct {
IdentityId int64
RealName string
Number string
BackFile *actions.File
FrontFile *actions.File
Must *actions.Must
CSRF *actionutils.CSRF
}) {
defer this.CreateLogInfo(codes.UserIdentity_LogUpdateUserIdentityIndividual)
// 检查IdentityId
var identityId = params.IdentityId
var oldFileIds = []int64{0, 0}
if identityId > 0 {
identityResp, err := this.RPC().UserIdentityRPC().FindEnabledUserIdentity(this.UserContext(), &pb.FindEnabledUserIdentityRequest{
UserIdentityId: identityId,
})
if err != nil {
this.ErrorPage(err)
return
}
if identityResp.UserIdentity == nil {
this.Fail("错误的表单数据,请刷新后重试")
}
var status = identityResp.UserIdentity.Status
if status != userconfigs.UserIdentityStatusNone && status != userconfigs.UserIdentityStatusRejected {
this.Fail("当前状态下无法修改")
}
oldFileIds = identityResp.UserIdentity.FileIds
}
var mustUpload = identityId <= 0
params.Must.
Field("realName", params.RealName).
Require("请输入真实姓名").
Field("number", params.Number).
Require("请输入身份证号")
var number = regexp.MustCompile(`\s+`).ReplaceAllString(params.Number, "")
number = strings.ToUpper(number)
if !regexp.MustCompile(`^\d+(X)?$`).MatchString(number) {
this.FailField("number", "身份证号中只能含有数字和X字母")
}
var numberLen = len(number)
if numberLen != 15 && numberLen != 18 {
this.FailField("number", "身份证号只能是15或者18位")
}
// back
var backFile = params.BackFile
if backFile == nil {
if mustUpload {
this.Fail("请上传身份证-照片面")
}
} else {
var backContentType = strings.ToLower(backFile.ContentType)
_, ok := allowedContentTypes[backContentType]
if !ok {
this.Fail("请上传正确格式的图片")
}
// TODO 需要可以配置最大上传尺寸,包括界面上的提示
if backFile.Size > 8*sizes.M {
this.Fail("要上传的图片(身份证-照片面不能超过8MiB")
}
}
// front
var frontFile = params.FrontFile
if frontFile == nil {
if mustUpload {
this.Fail("请上传身份证-国徽面")
}
} else {
var frontContentType = strings.ToLower(frontFile.ContentType)
_, ok := allowedContentTypes[frontContentType]
if !ok {
this.Fail("请上传正确格式的图片")
}
// TODO 需要可以配置最大上传尺寸,包括界面上的提示
if frontFile.Size > 8*sizes.M {
this.Fail("要上传的图片(身份证-国徽面不能超过8MiB")
}
}
// 上传到服务器
var backFileId int64 = 0
var frontFileId int64 = 0
if backFile != nil {
fileId, ok, err := this.UploadFile(backFile, "user.idcard")
if err != nil {
this.ErrorPage(err)
return
}
if !ok {
this.Fail("上传失败,请刷新后重试")
}
backFileId = fileId
}
if frontFile != nil {
fileId, ok, err := this.UploadFile(frontFile, "user.idcard")
if err != nil {
this.ErrorPage(err)
return
}
if !ok {
this.Fail("上传失败,请刷新后重试")
}
frontFileId = fileId
}
if len(oldFileIds) == 2 {
if backFileId == 0 {
backFileId = oldFileIds[0]
}
if frontFileId == 0 {
frontFileId = oldFileIds[1]
}
}
var err error
if identityId > 0 {
_, err = this.RPC().UserIdentityRPC().UpdateUserIdentity(this.UserContext(), &pb.UpdateUserIdentityRequest{
UserIdentityId: identityId,
Type: userconfigs.UserIdentityTypeIDCard,
RealName: params.RealName,
Number: number,
FileIds: []int64{backFileId, frontFileId},
})
} else {
_, err = this.RPC().UserIdentityRPC().CreateUserIdentity(this.UserContext(), &pb.CreateUserIdentityRequest{
OrgType: userconfigs.UserIdentityOrgTypeIndividual,
Type: userconfigs.UserIdentityTypeIDCard,
RealName: params.RealName,
Number: number,
FileIds: []int64{backFileId, frontFileId},
})
}
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,104 @@
package login
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"regexp"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
var user = userResp.User
if user == nil {
this.NotFound("user", this.UserId())
return
}
this.Data["user"] = maps.Map{
"username": user.Username,
"fullname": user.Fullname,
}
config, _ := configloaders.LoadRegisterConfig()
if config != nil {
this.Data["complexPassword"] = config.ComplexPassword
} else {
this.Data["complexPassword"] = false
}
this.Show()
}
func (this *IndexAction) RunPost(params struct {
Username string
Password string
Password2 string
Must *actions.Must
}) {
defer this.CreateLogInfo(codes.UserLogin_LogUpdateLogin)
var must = params.Must
must.Field("username", params.Username).
Require("请输入用户名").
Match("^[a-zA-Z0-9_]+$", "用户名中只能包含英文字母、数字、下划线").
MinLength(6, "用户名长度不能少于6位")
existsResp, err := this.RPC().UserRPC().CheckUserUsername(this.UserContext(), &pb.CheckUserUsernameRequest{
UserId: this.UserId(),
Username: params.Username,
})
if err != nil {
this.ErrorPage(err)
return
}
if existsResp.Exists {
this.FailField("username", "此用户名已经被别的用户使用,请换一个")
}
// 密码
if len(params.Password) > 0 {
must.
Field("password", params.Password).
Require("请输入密码").
MinLength(6, "密码长度不能小于6位")
config, err := configloaders.LoadRegisterConfig()
if err == nil {
if config.ComplexPassword && (!regexp.MustCompile(`[a-z]`).MatchString(params.Password) || !regexp.MustCompile(`[A-Z]`).MatchString(params.Password)) {
this.FailField("password", "为了您的账号安全,密码中必须包含大写和小写字母")
}
}
if params.Password != params.Password2 {
this.FailField("password2", "两次输入的密码不一致")
}
}
_, err = this.RPC().UserRPC().UpdateUserLogin(this.UserContext(), &pb.UpdateUserLoginRequest{
UserId: this.UserId(),
Username: params.Username,
Password: params.Password,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,18 @@
package login
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(settingutils.NewHelper("login")).
Prefix("/settings/login").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,28 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mfa
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
)
type DisableAction struct {
actionutils.ParentAction
}
func (this *DisableAction) RunPost(params struct{}) {
_, err := this.RPC().LoginRPC().UpdateLogin(this.UserContext(), &pb.UpdateLoginRequest{
Login: &pb.Login{
Type: "otp",
IsOn: false,
UserId: this.UserId(),
},
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,50 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mfa
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"github.com/xlzd/gotp"
)
type EnableAction struct {
actionutils.ParentAction
}
func (this *EnableAction) RunPost(params struct{}) {
// 修改OTP
otpLoginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.UserContext(), &pb.FindEnabledLoginRequest{
Type: "otp",
})
if err != nil {
this.ErrorPage(err)
return
}
{
var otpLogin = otpLoginResp.Login
if otpLogin == nil {
otpLogin = &pb.Login{
Id: 0,
Type: "otp",
ParamsJSON: maps.Map{
"secret": gotp.RandomSecret(16), // TODO 改成可以设置secret长度
}.AsJSON(),
IsOn: true,
UserId: this.UserId(),
}
} else {
// 如果已经有了,就覆盖,这样可以保留既有的参数
otpLogin.IsOn = true
}
_, err = this.RPC().LoginRPC().UpdateLogin(this.UserContext(), &pb.UpdateLoginRequest{Login: otpLogin})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Success()
}

View File

@@ -0,0 +1,48 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mfa
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var user = userResp.User
if user == nil {
this.NotFound("User", this.UserId())
return
}
// OTP
this.Data["otp"] = nil
if user.OtpLogin != nil && user.OtpLogin.IsOn {
loginParams := maps.Map{}
err = json.Unmarshal(user.OtpLogin.ParamsJSON, &loginParams)
if err != nil {
this.ErrorPage(err)
return
}
this.Data["otp"] = maps.Map{
"isOn": true,
"params": loginParams,
}
}
this.Show()
}

View File

@@ -0,0 +1,21 @@
package mfa
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(settingutils.NewHelper("mfa")).
Prefix("/settings/mfa").
GetPost("", new(IndexAction)).
Get("/otpQrcode", new(OtpQrcodeAction)).
Post("/enable", new(EnableAction)).
Post("/disable", new(DisableAction)).
EndAll()
})
}

View File

@@ -0,0 +1,83 @@
package mfa
import (
"encoding/json"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/utils/otputils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/TeaGo/types"
"github.com/skip2/go-qrcode"
"github.com/xlzd/gotp"
)
type OtpQrcodeAction struct {
actionutils.ParentAction
}
func (this *OtpQrcodeAction) Init() {
this.Nav("", "", "")
}
func (this *OtpQrcodeAction) RunGet(params struct {
Download bool
}) {
var userId = this.UserId()
loginResp, err := this.RPC().LoginRPC().FindEnabledLogin(this.UserContext(), &pb.FindEnabledLoginRequest{
Type: "otp",
})
if err != nil {
this.ErrorPage(err)
return
}
var login = loginResp.Login
if login == nil || !login.IsOn {
this.NotFound("userLogin", userId)
return
}
var loginParams = maps.Map{}
err = json.Unmarshal(login.ParamsJSON, &loginParams)
if err != nil {
this.ErrorPage(err)
return
}
var secret = loginParams.GetString("secret")
// 当前用户信息
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var user = userResp.User
if user == nil {
this.NotFound("user", userId)
return
}
uiConfig, err := configloaders.LoadUIConfig()
if err != nil {
this.ErrorPage(err)
return
}
var productName = uiConfig.ProductName
if len(productName) == 0 {
productName = "GoEdge用户"
}
var url = gotp.NewDefaultTOTP(secret).ProvisioningUri(user.Username, productName)
data, err := qrcode.Encode(otputils.FixIssuer(url), qrcode.Medium, 256)
if err != nil {
this.ErrorPage(err)
return
}
if params.Download {
var filename = "OTP-USER-" + user.Username + ".png"
this.AddHeader("Content-Disposition", "attachment; filename=\""+filename+"\";")
}
this.AddHeader("Content-Type", "image/png")
this.AddHeader("Content-Length", types.String(len(data)))
_, _ = this.Write(data)
}

View File

@@ -0,0 +1,132 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mobileverify
import (
"errors"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/utils"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/types"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct {
ViewOnly bool
}) {
this.Data["isModifying"] = !params.ViewOnly
this.Data["verifiedMobile"] = ""
// 是否可以激活手机号码
registerConfig, _ := configloaders.LoadRegisterConfig()
this.Data["canVerify"] = false
if registerConfig == nil {
this.Show()
return
}
var canVerify = registerConfig.MobileVerification.IsOn
if !canVerify {
this.Show()
return
}
this.Data["canVerify"] = true
this.Data["forceVerify"] = registerConfig.MobileVerification.Force
// 当前已激活手机号码
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{
UserId: this.UserId(),
})
if err != nil {
this.ErrorPage(err)
return
}
if userResp.User == nil {
this.ErrorPage(errors.New("can not find user info"))
return
}
this.Data["verifiedMobile"] = userResp.User.VerifiedMobile
// 有效期
var smsLife = userconfigs.MobileVerificationDefaultLife
if smsLife%60 == 0 {
this.Data["smsLife"] = types.String(smsLife/60) + "分钟"
} else {
this.Data["smsLife"] = types.String(smsLife) + "秒钟"
}
this.Data["smsCodeLength"] = userconfigs.MobileVerificationCodeLength
this.Show()
}
func (this *IndexAction) RunPost(params struct {
Mobile string
Must *actions.Must
CSRF *actionutils.CSRF
}) {
if len(params.Mobile) == 0 {
this.FailField("mobile", "请输入要激活的手机号码")
return
}
if !utils.IsValidMobile(params.Mobile) {
this.FailField("mobile", "手机号码格式不正确")
return
}
// 检查手机号码是否已被使用
checkResp, err := this.RPC().UserRPC().CheckUserMobile(this.UserContext(), &pb.CheckUserMobileRequest{Mobile: params.Mobile})
if err != nil {
this.ErrorPage(err)
return
}
if checkResp.Exists {
this.FailField("mobile", "当前手机号码已被别的用户使用,请换一个")
return
}
// 如果同自己已认证手机号码一致,则不重复发送
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
if userResp.User == nil {
this.Fail("can not find current user")
return
}
if userResp.User.VerifiedMobile == params.Mobile {
this.FailField("mobile", "此手机号码已激活,无需再次激活")
return
}
// 发送激活短信
resp, err := this.RPC().UserMobileVerificationRPC().SendUserMobileVerification(this.UserContext(), &pb.SendUserMobileVerificationRequest{Mobile: params.Mobile})
if err != nil {
this.ErrorPage(err)
return
}
if !resp.IsOk {
if len(resp.ErrorCode) > 0 {
this.Fail("短信发送失败,请稍后重试(错误代号:" + resp.ErrorCode + "")
} else {
this.Fail("短信发送失败,请稍后重试")
}
return
}
this.Success()
}

View File

@@ -0,0 +1,19 @@
package mobileverify
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(settingutils.NewHelper("profile")).
Prefix("/settings/mobile-verify").
GetPost("", new(IndexAction)).
Post("/verify", new(VerifyAction)).
EndAll()
})
}

View File

@@ -0,0 +1,41 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package mobileverify
import (
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"regexp"
)
type VerifyAction struct {
actionutils.ParentAction
}
func (this *VerifyAction) RunPost(params struct {
Mobile string
Code string
}) {
if len(params.Code) != userconfigs.MobileVerificationCodeLength ||
!regexp.MustCompile(`^\d+$`).MatchString(params.Code) {
this.FailField("code", "请输入正确的验证码")
return
}
resp, err := this.RPC().UserMobileVerificationRPC().VerifyUserMobile(this.UserContext(), &pb.VerifyUserMobileRequest{
Mobile: params.Mobile,
Code: params.Code,
})
if err != nil {
this.ErrorPage(err)
return
}
if len(resp.ErrorCode) > 0 {
this.FailField("code", resp.ErrorMessage)
return
}
this.Success()
}

View File

@@ -0,0 +1,116 @@
package profile
import (
"github.com/TeaOSLab/EdgeCommon/pkg/langs/codes"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeUser/internal/configloaders"
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type IndexAction struct {
actionutils.ParentAction
}
func (this *IndexAction) Init() {
this.Nav("", "", "")
}
func (this *IndexAction) RunGet(params struct{}) {
userResp, err := this.RPC().UserRPC().FindEnabledUser(this.UserContext(), &pb.FindEnabledUserRequest{UserId: this.UserId()})
if err != nil {
this.ErrorPage(err)
return
}
var user = userResp.User
if user == nil {
this.NotFound("user", this.UserId())
return
}
this.Data["user"] = maps.Map{
"fullname": user.Fullname,
"email": user.Email,
"verifiedEmail": user.VerifiedEmail,
"mobile": user.Mobile,
"verifiedMobile": user.VerifiedMobile,
"isVerified": user.IsVerified,
"isRejected": user.IsRejected,
"rejectReason": user.RejectReason,
}
// 邮箱是否需要激活
registerConfig, err := configloaders.LoadRegisterConfig()
if err != nil {
this.ErrorPage(err)
return
}
this.Data["emailRequireVerification"] = registerConfig != nil && registerConfig.EmailVerification.IsOn
// 现在正等待激活的邮箱
latestEmailVerificationResp, err := this.RPC().UserEmailVerificationRPC().FindLatestUserEmailVerification(this.UserContext(), &pb.FindLatestUserEmailVerificationRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var latestEmailVerification = latestEmailVerificationResp.UserEmailVerification
if latestEmailVerification == nil {
this.Data["latestEmailVerification"] = nil
} else {
this.Data["latestEmailVerification"] = maps.Map{
"email": latestEmailVerification.Email,
"expiresTime": timeutil.FormatTime("Y-m-d H:i:s", latestEmailVerification.ExpiresAt),
"isSent": latestEmailVerification.IsSent,
}
}
// 手机号码是否需要激活
this.Data["mobileRequireVerification"] = registerConfig != nil && registerConfig.MobileVerification.IsOn
this.Show()
}
func (this *IndexAction) RunPost(params struct {
Fullname string
Mobile string
Email string
Must *actions.Must
}) {
defer this.CreateLogInfo(codes.User_LogUpdateUserProfile)
var must = params.Must
// 手机号
must.Field("mobile", params.Mobile).
Require("请输入手机号").
Mobile("请输入正确的手机号")
// 邮箱
must.Field("email", params.Email).
Require("请输入邮箱").
Email("请输入正确的邮箱")
// 全名
must.Field("fullname", params.Fullname).
Require("请输入姓名或者公司名称")
params.Must.
Field("fullname", params.Fullname).
Require("请输入你的姓名")
_, err := this.RPC().UserRPC().UpdateUserInfo(this.UserContext(), &pb.UpdateUserInfoRequest{
UserId: this.UserId(),
Fullname: params.Fullname,
Mobile: params.Mobile,
Email: params.Email,
})
if err != nil {
this.ErrorPage(err)
return
}
this.Success()
}

View File

@@ -0,0 +1,18 @@
package profile
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/settings/settingutils"
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
"github.com/iwind/TeaGo"
)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.
Helper(helpers.NewUserMustAuth("")).
Helper(settingutils.NewHelper("profile")).
Prefix("/settings/profile").
GetPost("", new(IndexAction)).
EndAll()
})
}

View File

@@ -0,0 +1,35 @@
package settingutils
import (
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
"github.com/iwind/TeaGo/actions"
)
type Helper struct {
tab string
}
func NewHelper(tab string) *Helper {
return &Helper{
tab: tab,
}
}
func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
goNext = true
var action = actionPtr.Object()
// 左侧菜单
action.Data["teaMenu"] = "settings"
// 标签栏
tabbar := actionutils.NewTabbar()
tabbar.Add("个人资料", "", "/settings/profile", "", this.tab == "profile")
tabbar.Add("实名认证", "", "/settings/identity", "", this.tab == "identity")
tabbar.Add("多因子认证", "", "/settings/mfa", "", this.tab == "mfa")
tabbar.Add("登录设置", "", "/settings/login", "", this.tab == "login")
actionutils.SetTabbar(actionPtr, tabbar)
return
}