diff --git a/backend/__debug_bin3332691057 b/backend/__debug_bin3332691057 new file mode 100755 index 0000000..54cbb6d Binary files /dev/null and b/backend/__debug_bin3332691057 differ diff --git a/backend/modules/middlewares/m_wechat_auth.go b/backend/modules/middlewares/m_wechat_auth.go new file mode 100644 index 0000000..ce46aaf --- /dev/null +++ b/backend/modules/middlewares/m_wechat_auth.go @@ -0,0 +1,46 @@ +package middlewares + +import ( + "strings" + + "backend/providers/wechat" + + "github.com/gofiber/fiber/v3" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func (f *Middlewares) WeChatAuth(c fiber.Ctx) error { + log.WithField("module", "middleware.AuthUserInfo").Debugf("%s, query: %v", c.OriginalURL(), c.Queries()) + state := c.Query("state") + code := c.Query("code") + log.WithField("module", "middleware.AuthUserInfo").Debugf("code: %s, state: %s", code, state) + + if state == "" && code == "" { + url := string(c.Request().URI().FullURI()) + if f.app.IsDevMode() && f.app.BaseURI != nil { + url = strings.ReplaceAll(url, "http", "https") + url = strings.ReplaceAll(url, c.BaseURL(), *f.app.BaseURI) + } + + log.WithField("module", "middleware.SilentAuth").Debug("redirect_uri: ", url) + + to, err := f.client.ScopeAuthorizeURL( + wechat.ScopeAuthorizeURLWithRedirectURI(url), + wechat.ScopeAuthorizeURLWithState("sns_basic_auth"), + ) + if err != nil { + return errors.Wrap(err, "failed to get wechat auth url") + } + log.WithField("module", "middleware.SilentAuth").Debug("redirectTo: ", to.String()) + + return c.Redirect().To(to.String()) + + } + + if state != "sns_basic_auth" || code == "" { + return errors.New("invalid request") + } + + return c.Next() +} diff --git a/backend/modules/middlewares/m_wechat_auth_userinfo.go b/backend/modules/middlewares/m_wechat_auth_userinfo.go deleted file mode 100644 index af5b521..0000000 --- a/backend/modules/middlewares/m_wechat_auth_userinfo.go +++ /dev/null @@ -1,93 +0,0 @@ -package middlewares - -import ( - _ "embed" - "os" - "path/filepath" - "strings" - - "backend/pkg/pg" - "backend/providers/jwt" - - "github.com/gofiber/fiber/v3" - "github.com/jinzhu/copier" - "github.com/pkg/errors" - "github.com/samber/lo" - log "github.com/sirupsen/logrus" -) - -func (f *Middlewares) WeChatAuthUserInfo(c fiber.Ctx) error { - // 如果请求存在 Authorization 头,则跳过 - if len(c.GetReqHeaders()["Authorization"]) != 0 { - return c.Next() - } - - log.WithField("module", "middleware.AuthUserInfo").Debugf("%s, query: %v", c.OriginalURL(), c.Queries()) - state := c.Query("state") - code := c.Query("code") - - if state == "" && code == "" { - return c.Next() - } - - if state != "sns_basic_auth" { - return c.Next() - } - log.WithField("module", "middleware.AuthUserInfo").Debugf("code: %s, state: %s", code, state) - - // get the openid - token, err := f.client.AuthorizeCode2Token(code) - if err != nil { - return errors.Wrap(err, "failed to get openid") - } - log.Debugf("tokenInfo %+v", token) - - paths := lo.Filter(strings.Split(c.Path(), "/"), func(s string, _ int) bool { - return s != "" - }) - if len(paths) < 2 || paths[0] != "t" { - return errors.New("invalid path") - } - - tenantSlug := paths[1] - if tenantSlug == "" { - return errors.New("tenant is empty") - } - - tenant, err := f.userSvc.GetTenantBySlug(c.Context(), tenantSlug) - if err != nil { - return errors.Wrap(err, "failed to get tenant id") - } - - var oauthInfo pg.UserOAuth - if err := copier.Copy(&oauthInfo, token); err != nil { - return errors.Wrap(err, "failed to copy oauth info") - } - log.Debugf("oauthInfo %+v", oauthInfo) - - user, err := f.userSvc.GetOrNew(c.Context(), tenant.ID, token.Openid, oauthInfo) - if err != nil { - return errors.Wrap(err, "failed to get user") - } - - claim := f.jwt.CreateClaims(jwt.BaseClaims{ - OpenID: user.OpenID, - Tenant: tenantSlug, - UserID: user.ID, - TenantID: tenant.ID, - }) - jwtToken, err := f.jwt.CreateToken(claim) - if err != nil { - return errors.Wrap(err, "failed to create token") - } - - b, err := os.ReadFile(filepath.Join(f.storagePath.Asset, "index.html")) - if err != nil { - return errors.Wrap(err, "failed to read file") - } - - html := strings.ReplaceAll(string(b), "{{JWT}}", jwtToken) - - c.Set("Content-Type", "text/html") - return c.SendString(html) -} diff --git a/backend/modules/middlewares/m_wechat_silent_auth.go b/backend/modules/middlewares/m_wechat_silent_auth.go deleted file mode 100644 index 4f47318..0000000 --- a/backend/modules/middlewares/m_wechat_silent_auth.go +++ /dev/null @@ -1,39 +0,0 @@ -package middlewares - -import ( - "strings" - - "backend/providers/wechat" - - "github.com/gofiber/fiber/v3" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" -) - -func (f *Middlewares) WeChatSilentAuth(c fiber.Ctx) error { - // if cookie not exists key "openid", then redirect to the wechat auth page - token := c.GetReqHeaders()["Authorization"] - if len(token) != 0 { - return c.Next() - } - - // get current full url - url := string(c.Request().URI().FullURI()) - if f.app.IsDevMode() && f.app.BaseURI != nil { - url = strings.ReplaceAll(url, "http", "https") - url = strings.ReplaceAll(url, c.BaseURL(), *f.app.BaseURI) - } - - log.WithField("module", "middleware.SilentAuth").Debug("redirect_uri: ", url) - - to, err := f.client.ScopeAuthorizeURL( - wechat.ScopeAuthorizeURLWithRedirectURI(url), - wechat.ScopeAuthorizeURLWithState("sns_basic_auth"), - ) - if err != nil { - return errors.Wrap(err, "failed to get wechat auth url") - } - log.WithField("module", "middleware.SilentAuth").Debug("redirectTo: ", to.String()) - - return c.Redirect().To(to.String()) -} diff --git a/backend/modules/middlewares/mid_debug.go b/backend/modules/middlewares/mid_debug.go index 92716b1..519442e 100644 --- a/backend/modules/middlewares/mid_debug.go +++ b/backend/modules/middlewares/mid_debug.go @@ -2,12 +2,14 @@ package middlewares import ( "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" ) func (f *Middlewares) DebugMode(c fiber.Ctx) error { // fullURI := c.Request().URI().FullURI() // host := c.BaseURL() // fmt.Println(strings.Split(c.Path(), "/")) - // return c.SendString(c.Params("tenant", "no tenant: "+c.Path())) + // return c.SendString("ABC" + c.Params("+")) + log.SetLevel(log.DebugLevel) return c.Next() } diff --git a/backend/modules/middlewares/provider.gen.go b/backend/modules/middlewares/provider.gen.go index 58001f2..b1f58f6 100755 --- a/backend/modules/middlewares/provider.gen.go +++ b/backend/modules/middlewares/provider.gen.go @@ -4,6 +4,7 @@ import ( "backend/modules/users" "backend/providers/app" "backend/providers/jwt" + "backend/providers/storage" "backend/providers/wechat" "git.ipao.vip/rogeecn/atom/container" @@ -15,13 +16,15 @@ func Provide(opts ...opt.Option) error { app *app.Config, client *wechat.Client, jwt *jwt.JWT, + storagePath *storage.Config, userSvc *users.Service, ) (*Middlewares, error) { obj := &Middlewares{ - app: app, - client: client, - jwt: jwt, - userSvc: userSvc, + app: app, + client: client, + jwt: jwt, + storagePath: storagePath, + userSvc: userSvc, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/modules/wechat/controller.go b/backend/modules/wechat/controller.go new file mode 100644 index 0000000..f20db5a --- /dev/null +++ b/backend/modules/wechat/controller.go @@ -0,0 +1,75 @@ +package users + +import ( + "os" + "path/filepath" + "strings" + + "backend/modules/users" + "backend/pkg/pg" + "backend/providers/jwt" + "backend/providers/storage" + "backend/providers/wechat" + + "github.com/gofiber/fiber/v3" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// @provider +type Controller struct { + jwt *jwt.JWT + storagePath *storage.Config + userSvc *users.Service + client *wechat.Client +} + +func (c *Controller) Render(ctx fiber.Ctx) error { + code := ctx.Query("code") + + // get the openid + token, err := c.client.AuthorizeCode2Token(code) + if err != nil { + return errors.Wrap(err, "failed to get openid") + } + log.Debugf("tokenInfo %+v", token) + + tenantSlug := ctx.Params("tenant") + tenant, err := c.userSvc.GetTenantBySlug(ctx.Context(), tenantSlug) + if err != nil { + return errors.Wrap(err, "failed to get tenant id") + } + + var oauthInfo pg.UserOAuth + if err := copier.Copy(&oauthInfo, token); err != nil { + return errors.Wrap(err, "failed to copy oauth info") + } + log.Debugf("oauthInfo %+v", oauthInfo) + + user, err := c.userSvc.GetOrNew(ctx.Context(), tenant.ID, token.Openid, oauthInfo) + if err != nil { + return errors.Wrap(err, "failed to get user") + } + + claim := c.jwt.CreateClaims(jwt.BaseClaims{ + OpenID: user.OpenID, + Tenant: tenantSlug, + UserID: user.ID, + TenantID: tenant.ID, + }) + jwtToken, err := c.jwt.CreateToken(claim) + if err != nil { + return errors.Wrap(err, "failed to create token") + } + + b, err := os.ReadFile(filepath.Join(c.storagePath.Asset, "index.html")) + if err != nil { + return errors.Wrap(err, "failed to read file") + } + + html := strings.ReplaceAll(string(b), "{{JWT}}", jwtToken) + + ctx.Set("Content-Type", "text/html") + return ctx.SendString(html) +} diff --git a/backend/modules/wechat/provider.gen.go b/backend/modules/wechat/provider.gen.go new file mode 100755 index 0000000..2506977 --- /dev/null +++ b/backend/modules/wechat/provider.gen.go @@ -0,0 +1,32 @@ +package users + +import ( + "backend/modules/users" + "backend/providers/jwt" + "backend/providers/storage" + "backend/providers/wechat" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + client *wechat.Client, + jwt *jwt.JWT, + storagePath *storage.Config, + userSvc *users.Service, + ) (*Controller, error) { + obj := &Controller{ + client: client, + jwt: jwt, + storagePath: storagePath, + userSvc: userSvc, + } + return obj, nil + }); err != nil { + return err + } + + return nil +} diff --git a/backend/pkg/service/http/http.go b/backend/pkg/service/http/http.go index 3e79972..6cea40a 100644 --- a/backend/pkg/service/http/http.go +++ b/backend/pkg/service/http/http.go @@ -6,6 +6,7 @@ import ( "backend/modules/medias" "backend/modules/middlewares" "backend/modules/users" + wechatModule "backend/modules/wechat" "backend/providers/app" "backend/providers/hashids" "backend/providers/http" @@ -46,6 +47,7 @@ func Command() atom.Option { middlewares.Provide, users.Provide, medias.Provide, + wechatModule.Provide, )), ) } @@ -55,6 +57,7 @@ type Http struct { App *app.Config Storage *storage.Config + Wechat *wechatModule.Controller Service *http.Service Initials []contracts.Initial `group:"initials"` Routes []contracts.HttpRoute `group:"routes"` @@ -73,7 +76,7 @@ func Serve(cmd *cobra.Command, args []string) error { engine.Use(mid.ProcessResponse) engine.Use(mid.WeChatVerify) - engine.Use("/t+", mid.WeChatAuthUserInfo, mid.WeChatSilentAuth) + engine.Use([]string{"/t/:tenant", "/t/:tenant/*"}, mid.WeChatAuth, http.Wechat.Render) http.Service.Engine.Use(favicon.New(favicon.Config{ Data: []byte{}, diff --git a/frontend/src/components/ChargeCode.vue b/frontend/src/components/ChargeCode.vue index 585a761..7e1ff39 100644 --- a/frontend/src/components/ChargeCode.vue +++ b/frontend/src/components/ChargeCode.vue @@ -3,7 +3,7 @@ - + @@ -32,14 +32,6 @@ export default defineComponent({ return code.amount / 100 + " 元/ " + code.amount + " 点"; }; - const copyCode = (code) => { - navigator.clipboard.writeText(code.code).then(() => { - showSuccessToast('充值码已复制'); - }).catch((err) => { - showFailToast('复制失败'); - }); - }; - onMounted(() => { loadChargeCodes(); }); @@ -47,7 +39,6 @@ export default defineComponent({ return { codes, getCodeAmountTitle, - copyCode, loadChargeCodes, } } diff --git a/frontend/src/components/ChargeNoticeBar.vue b/frontend/src/components/ChargeNoticeBar.vue index 3363186..52e780a 100644 --- a/frontend/src/components/ChargeNoticeBar.vue +++ b/frontend/src/components/ChargeNoticeBar.vue @@ -1,5 +1,5 @@ @@ -12,15 +12,6 @@ export default defineComponent({ contact: String, }, setup(props) { - const copyCode = (code) => { - navigator.clipboard.writeText(code).then(() => { - showSuccessToast('客服微信已复制'); - }).catch((err) => { - showFailToast('复制失败'); - }); - }; - - return { copyCode, } diff --git a/frontend/src/router/routes.js b/frontend/src/router/routes.js index 030cc05..af26776 100644 --- a/frontend/src/router/routes.js +++ b/frontend/src/router/routes.js @@ -1,5 +1,8 @@ import NotFound from '@/views/NotFound.vue'; +import PlayView from '@/views/PlayView.vue'; +import BoughtView from '@/views/tabs/BoughtView.vue'; import HomeView from '@/views/tabs/HomeView.vue'; +import UserView from '@/views/tabs/UserView.vue'; import TabView from '@/views/TabView.vue'; const routes = [ @@ -22,7 +25,7 @@ const routes = [ { path: 'bought', name: 'tab.bought', - component: () => import('@/views/tabs/BoughtView.vue'), + component: BoughtView, meta: { title: '已购买', keepAlive: true, @@ -35,7 +38,7 @@ const routes = [ // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import('@/views/tabs/UserView.vue'), + component: UserView, meta: { title: '个人中心', keepAlive: false, @@ -46,7 +49,7 @@ const routes = [ { path: '/t/:tenant/play/:hash', name: 'play', - component: () => import('@/views/PlayView.vue'), + component: PlayView, meta: { title: '播放', keepAlive: false, diff --git a/frontend/src/utils/copy.js b/frontend/src/utils/copy.js new file mode 100644 index 0000000..afa6731 --- /dev/null +++ b/frontend/src/utils/copy.js @@ -0,0 +1,44 @@ +const copy = (text) => { + // 数字没有 .length 不能执行selectText 需要转化成字符串 + const textString = text.toString(); + const input = document.createElement('input'); + input.id = 'copy-input'; + input.readOnly = true; // 防止ios聚焦触发键盘事件 + input.style.position = 'absolute'; + input.style.left = '-1000px'; + input.style.zIndex = '-1000'; + document.body.appendChild(input); + input.value = textString; + + // ios必须先选中文字且不支持 input.select(); + selectText(input, 0, textString.length); + + input.blur(); + document.body.removeChild(input); // 使用完成后,移除 input 元素,避免占用页面高度 + + // input自带的select()方法在苹果端无法进行选择,所以需要自己去写一个类似的方法 + // 选择文本。createTextRange(setSelectionRange)是input方法 + function selectText(textBox, startIndex, stopIndex) { + if (textBox.createTextRange) { + //ie + const range = textBox.createTextRange(); + range.collapse(true); + range.moveStart('character', startIndex); //起始光标 + range.moveEnd('character', stopIndex - startIndex); //结束光标 + range.select(); //不兼容苹果 + } else { + //firefox/chrome + textBox.setSelectionRange(startIndex, stopIndex); + textBox.focus(); + } + } + + console.log(document.execCommand('copy'), 'execCommand'); + if (document.execCommand('copy')) { + document.execCommand('copy'); + return true + } + return false +}; + +export default copy; \ No newline at end of file