分类: 前端

  • 在Chrome扩展中使用Parse Platform-邮箱验证码登录

    本来想使用oauth来实现登录,但是国内提供oauth服务要么比较小众,要么居然收费的?传统的用户注册登录使用起来过于繁琐了,很容易把用户挡在最开始的地方,最后决定添加邮箱验证码登录。

    Parse Platform的文档中提到了一个Parse.User.become()方法,但是需要传递一个session token进去,翻了很多遍文档也没发现该如何获取这个session_token,最后在Github仓库的issue中搜到2023年Parse增加了一个loginAs方法,可以通过传递一个userId来将用户登陆进系统,那么通过云函数和Parse.User.become()就可以实现邮箱验证码登录了。

    服务端

    1. 创建发送验证码函数

    const VerifyCode = Parse.Object.extend('VerifyCode')
    
    /**
     * 发送邮件验证码
     */
    Parse.Cloud.define('verifyCode', async (request) => {
        const email = request.params.email
        const code = generateRandomString(6)
    
        // 记录验证码
        const verifyCode = new VerifyCode()
        verifyCode.set('email', email)
        verifyCode.set('code', code)
        verifyCode.set('expiredAt', new Date(Date.now() + 1000 * 60 * 5))
        await verifyCode.save(null, { useMasterKey: true })
    
        // 删除过期的验证码记录
        const query = new Parse.Query(VerifyCode)
        query.lessThan('expiredAt', new Date())
        await Parse.Object.destroyAll(await query.find({ useMasterKey: true }), { useMasterKey: true })
    
        // 发送邮件
        await Parse.Cloud.sendEmail({
            templateName: "verifyCode",
            placeholders: { code },
            recipient: email
        });
    }, {
        fields: {
            email: {
                required: true,
                type: String
            }
        },
        requireUser: false
    })

    2. 创建登录函数

    const VerifyCode = Parse.Object.extend('VerifyCode')
    
    Parse.Cloud.define('login', async (request) => {
        if (!request.params.email) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少email字段');
        }
        if (!request.params.code) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少code字段');
        }
    
        const query = new Parse.Query(Parse.User)
        query.equalTo('email', request.params.email)
        let user = await query.first({ useMasterKey: true })
        if (user === undefined) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '用户不存在');
        }
    
        // 验证
        const codeQuery = new Parse.Query(VerifyCode)
        codeQuery.equalTo('email', request.params.email)
        codeQuery.equalTo('code', request.params.code)
        codeQuery.greaterThan('expiredAt', new Date())
        const code = await codeQuery.first({ useMasterKey: true })
        if (code === undefined) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '验证码已过期');
        }
    
        // 修改邮箱验证字段为验证通过状态
        user.set('emailVerified', true)
        user.save(null, { useMasterKey: true })
    
        // 登录用户
        user = await Parse.User.loginAs(user.id)
        return user.getSessionToken()
    }, {
        fields: {
            email: {
                required: true,
                type: String
            },
            code: {
                required: true,
                type: String
            }
        }
    })

    客户端

    export async function emailLogin(email: string, code: string) {
        const token = await Parse.Cloud.run('login', { email, code })
    
        return await Parse.User.become(token)
    }
  • 在Chrome扩展中使用Parse Platform-集成Gitee登录

    国内提供Oauth认证服务的好少,使用第三方登录降低了认证服务的复杂度,也降低了用户的决策难度,这里用Gitee作为一个示例。应该适用于大多数的Web应用,只有客户端获取authorization_code部分的代码可能不一致。Parse官方文档里的自定义认证写的语焉不详,折腾了好久。

    服务端

    1. Parse Server配置认证信息

    const gitee = require('./auth/gitee');
    
    {
      auth: {
       gitee: {
         module: gitee, // OR object,
         client_id: "", // Gitee第三方应用ID
         client_secret: "", // Gitee第三方应用的密钥
       }
      }
    }

    2. 实现自定义登录(AuthAdapter)

    主要需要实现两个函数:validateAuthData和validateAppId。

    const qs = require('querystring')
    const Parse = require('parse/node')
    
    async function getAuthData(code, redirect_uri, options) {
        const token = await codeToToken(code, redirect_uri, options)
        const user = await userInfo(token.token_type, token.access_token)
    
        return {
            id: user.id,
            access_token: token.access_token
        }
    }
    
    async function codeToToken(code, redirect_uri, options) {
        const url = `https://gitee.com/oauth/token`
        const query = qs.stringify({
            grant_type: 'authorization_code',
            code,
            redirect_uri,
            client_id: options.client_id,
            client_secret: options.client_secret
        })
        const res = await fetch(`${url}?${query}`, { method: 'POST' })
    
        return await res.json()
    }
    
    async function userInfo(type, token) {
        const url = 'https://gitee.com/api/v5/user'
    
        const res = await fetch(url, {
            method: 'GET',
            headers: {
                Authorization: `${type} ${token}`
            }
        })
    
        return await res.json()
    }
    
    // Returns a promise that fulfills if this user id is valid.
    async function validateAuthData(authData, options) {
        if (!authData.id) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少id字段');
        }
        if (!authData.access_token) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少access_token字段');
        }
    
        const user = await userInfo('bearer', authData.access_token)
        if (user && String(user.id) === String(authData.id)) {
            return
        }
    
        throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'authData is invalid for this user.');
    }
    
    // Returns a promise that fulfills iff this app id is valid.
    function validateAppId(appIds, authData, options) {
        console.log('validateAppId:', appIds, authData, options)
        return Promise.resolve();
    }
    
    module.exports = {
        validateAuthData,
        validateAppId,
        getAuthData
    }

    3. 添加一个云函数,用于获取authData

    const gitee = require('../auth/gitee')
    const config = require('./config')
    
    Parse.Cloud.define("giteeAuthData", async (request) => {
        return await gitee.getAuthData(request.params.code, request.params.redirect_uri, config.auth.gitee)
    })

    客户端

    async function login() {
            const redirectUrl = chrome.identity.getRedirectURL()
            logger.log('redirect_url:', redirectUrl)
    
            // 获取认证码
            const authUrl = await _oauthCode(redirectUrl)
            if (authUrl === undefined) {
                throw new Error('login failed')
            }
            const url = new URL(authUrl)
            const code = url.searchParams.get('code')
            if (code == null) {
                throw new Error('login failed')
            }
            logger.log('code:', code)
    
            // 获取认证信息
            const authData = await _oauthData(code, redirectUrl)
    
            user.value = await Parse.User.logInWith('gitee', {
                authData
            })
    }
    
    
    async function _oauthData(code: string, redirect_uri: string) {
            const authData = await Parse.Cloud.run('giteeAuthData', {
                code,
                redirect_uri
            })
            logger.log('authData:', authData)
    
            return authData
    }

    参考

    [1] Parse 自定义认证文档地址, Parse Server Guide | Parse (parseplatform.org)

    [2] Gitee Oauth文档,Gitee OAuth 文档

  • APP Store应用上架需要注意的点

    最近公司的应用上架APP Store过程中一直遇到审核问题,记录一下遇到的问题,方便后续其他应用开发避坑。

    1、与安卓通用的审核条件

    2、虚拟支付

    1. 所有的虚拟商品直接使用苹果的内购方式付款,避免后续改造的问题(需要给苹果30%抽成)
    2. 提前考虑宣传物料和价格在不同货币区域的展示,或者避免物料中包含价格
    3. 至少审核阶段,不要在应用界面里放跳转到其他应用的链接,苹果审核会认为这涉及到其他支付方式

    3、强制登录问题

    微信小程序也是一样的要求,所以最好设计阶段就开始考虑这个问题。

    1. 不需要用户信息的数据不设计为强制鉴权
    2. 将引导用户登录放到用户进行必须鉴权的操作时
  • 使用typescript开发chrome扩展

    记录一下使用typescript开发chrome扩展的相关配置。

    1. 安装依赖

    必定需要用到的开发依赖项:

    • chrome-types
    • copy-webpack-plugin
    • ts-loader
    • typescript
    • webpack-cli
    npm install chrome-types webpack-cli ts-loader typescript copy-webpack-plugin --save-dev

    2. 打包配置

    2.1 首先创建ts配置文件

    npx tsc --init

    2.2 创建webpack打包配置文件

    const path = require('path');
    const CopyPlugin = require("copy-webpack-plugin");
    
    module.exports = {
      mode: 'production',
      entry: {
        index: {
          import: './src/index.ts',
          filename: 'index.js'
        },
        background: {
          import: './src/background.ts',
          filename: 'background.js'
        }
      },
      plugins: [
        new CopyPlugin({
          patterns: [
            { from: "public", to: "" },
          ],
        }),
      ],
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/,
          },
        ],
      },
      resolve: {
        extensions: ['.tsx', '.ts', '.js'],
      },
      output: {
        clean: true
      },
    };

    3. 配置打包命令

    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack build"
      }

    4. 基本目录结构截图

    chrome使用typescript目录结构
    chrome使用typescript目录结构
  • APP分发前的准备工作

    1. 登录、注册、注销
    2. 隐私权政策和服务协议
    3. 登录注册页面弹窗(勾选已读服务协议和隐私权政策,弹窗动作必须)
    4. 应用市场和对应账号
      • 苹果开发者会员
      • 主体认证
      • 涉及收费可能需要ICP证
    5. 软著登记证
  • 网页、APP跳转应用商店

    安卓

    1. 跳转到应用页面:market://details?id=
    2. 跳转到搜索:market://search?q=

    IOS

    1. 应用页面:itms-apps://itunes.apple.com/app/id 114211089
  • Electron中数据持久化的选择

    Electron是一个基于Chromium的桌面应用程序框架,它可以让开发人员在不需要熟练掌握Web开发技术的情况下,快速地开发出高质量的桌面应用程序。在Electron中,开发人员可以使用各种各样的数据存储方式,包括文件系统、数据库等。其中,数据库是一种非常常见的数据存储方式,它可以方便地存储和管理各种数据,包括文本、图片、音频、视频等。

    (更多…)
  • Google Chrome扩展开发

    Chrome扩展开发者控制台

    1. 需要支付一次性的5美元
    2. 需要使用非国内卡

    参考:

    1. Chrome开发者网站