Teo 0.2.16 发布!用户登录只需 5 行代码,高度可扩展!

来源: 投稿
作者: VictorTeo
2024-03-17 19:15:00

Teo是以结构为核心的声明式网络后端开发框架,支持Node.js、Python和Rust语言,支持手动编写和AI编写。它能够节省80%的开发时间,大大缩短开发时间和开发成本,符合当下的技术环境和社会环境,是新一代的网络开发框架。

在0.2.16版本中,我们新增了用户登录、token验证等功能。多的不说,请看演示。

Teo schema

声明一个服务器,包含增删改查分组聚合是非常简单的,甚至不需要编程代码,使用编程代码,开发者可以访问ORM API和定义自定义的路由,编写自定义的路由先进后出中间件。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)}model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail))  email: String  @writeonly @onSet($presents.bcrypt.salt)  password: String}

如上所示,声明一个含有邮箱和密码的用户表,并产生增删改查分组聚合等API,是非常容易的。在schema中,我们也指定了服务器监听的端口和连接的数据库。Teo采用连接池来管理数据库连接,非常的高效和性能。使用命令`teo serve`即可启动服务器。

用户登录

为这个用户表增加登录功能,非常的简单,请看更新后的代码。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)} @identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))@identity.jwtSecret(ENV["JWT_SECRET"]!)model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail)) @identity.id  email: String  @writeonly @onSet($presents.bcrypt.salt)  @identity.checker($get(.value).presents.bcrypt.verify($self.get(.password).presents))  password: String   include handler identity.signIn  include handler identity.identity} middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

我们增加了一些修饰符,指定了token的encode方式,这里采用业内最流行的JWT token,并指定了过期时间。我们标记了采用邮箱登录,采用密码验证,并且增加了两个由模版handler构成的新的route handler。这两个route handler完全不需要手动代码编写。在文件内增加中间件,这个中间件会验证token并且设置当前的登录者。这段代码,总共比之前的没有登录功能的时候,增加了5行代码。

辅助验证信息

在实际开发中,我们在登录的时候,经常要用户点击图片验证码或输入一些来自图片中的文字。这些辅助的验证信息,也可以通过Teo的API进行传递,话不多说,上例子。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)} @identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))@identity.jwtSecret(ENV["JWT_SECRET"]!)model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail)) @identity.id  email: String  @writeonly @onSet($presents.bcrypt.salt)  @identity.checker(    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))    .do($get(.companions).presents.get(.imageAuthToken).presents))  password: String  @virtual @writeonly @identity.companion  imageAuthToken: String?   include handler identity.signIn  include handler identity.identity} middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

我们又增加了四行代码。imageAuthToken是一个虚拟字段,它不会在数据库中出现,但是却会在请求中出现,这样的字段是配合API功能的。比如,输入旧密码更改当前密码,这个功能,就需要定义一个oldPassword这样的虚拟字段。在这个例子中,我们只是检查imageAuthToken是否存在,而实际应用中,我们通过编写pipeline item跟第三方接口验证验证码即可。

动态的token过期时间

有一些论坛网站,token过期时间是用户自己选择的,现在有这个需求的产品很少,但是Teo同样支持。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)} @identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))@identity.jwtSecret(ENV["JWT_SECRET"]!)model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail)) @identity.id  email: String  @writeonly @onSet($presents.bcrypt.salt)  @identity.checker(    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))    .do($get(.companions).presents.get(.imageAuthToken).presents))  password: String  @virtual @writeonly @identity.companion  imageAuthToken: String?  @virtual @writeonly @identity.companion  expired: Int64?   include handler identity.signIn  include handler identity.identity} middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

把JWT的expire参数改为动态获取,就可以得到用户传来的expire参数了。如果expire参数填一个很低的秒数,这个token很快就会过期。

账号封禁

实现封禁账号是很简单的,利用声明式的优势。告诉Teo,什么样的账号判定为被封禁即可。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)} @identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))@identity.jwtSecret(ENV["JWT_SECRET"]!)@identity.validateAccount($get(.enabled).presents.eq(true))model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail)) @identity.id  email: String  @writeonly @onSet($presents.bcrypt.salt)  @identity.checker(    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))    .do($get(.companions).presents.get(.imageAuthToken).presents))  password: String  @virtual @writeonly @identity.companion  imageAuthToken: String?  @virtual @writeonly @identity.companion  expired: Int64?  @migration(default: true)  enabled: Bool   include handler identity.signIn  include handler identity.identity} middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

在这里更新的例子中,通过新增的一行模型声明,Teo知道了,enabled字段为false的账号即是封禁账号。封禁的账号不能进行登录,其token也会失效。

第三方身份集成

实际开发中,我们经常要做微信登录,支付宝登录等平台身份登录。具体的前端跳转逻辑,我们不做演示。在后端,绑定账号就是更新字段,我们演示如何使用三方token进行登录。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)} @identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))@identity.jwtSecret(ENV["JWT_SECRET"]!)@identity.validateAccount(  $message($get(.enabled).presents.eq(true), "this account is blocked"))model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail)) @identity.id  email: String  @writeonly @onSet($presents.bcrypt.salt)  @identity.checker(    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))    .do($get(.companions).presents.get(.imageAuthToken).presents))  password: String  @virtual @writeonly @identity.companion  imageAuthToken: String?  @virtual @writeonly @identity.companion  expired: Int64?  @migration(default: true) @default(true)  enabled: Bool  @identity.id @unique  thirdPartyId: String?  @virtual @writeonly @identity.checker($get(.value).presents.valid)  thirdPartyToken: String?   include handler identity.signIn  include handler identity.identity} middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

在表中新增三方id和三方token,使用自定义的pipeline item与三方沟通获得token是否有效。一旦有效,就给用户在我们系统中登录,获得我们的token。这时,进行signIn请求,我们不用输入邮箱和密码,输入三方id和token即可。

手机验证码/邮箱验证码/手机密码/邮箱密码

Teo支持任意的身份字段和验证方式相匹配。我们来看一个验证码验证的案例。这一次需要编写编程代码。

connector {  provider: .sqlite,  url: "sqlite:./database.sqlite"} server {  bind: ("0.0.0.0", 5052)} entity {  provider: .rust,  dest: "./src/entities"} declare pipeline item validateAuthCode<T>: T -> String @identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))@identity.jwtSecret(ENV["JWT_SECRET"]!)@identity.validateAccount(  $message($get(.enabled).presents.eq(true), "this account is blocked"))model User {  @id @autoIncrement @readonly  id: Int  @unique @onSet($if($presents, $isEmail)) @identity.id  @presentWithout(.phoneNumber)  email: String?  @writeonly @onSet($presents.bcrypt.salt)  @identity.checker(    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))    .do($get(.companions).presents.get(.imageAuthToken).presents))  password: String?  @virtual @writeonly @identity.companion  imageAuthToken: String?  @virtual @writeonly @identity.companion  expired: Int64?  @migration(default: true) @default(true)  enabled: Bool  @identity.id @unique  thirdPartyId: String?  @virtual @writeonly @identity.checker($get(.value).presents.valid)  thirdPartyToken: String?  @onSet($if($presents, $regexMatch(/\\+?[0-9]+/))) @identity.id  @presentWithout(.email) @unique  phoneNumber: String?  @virtual @writeonly @identity.checker($validateAuthCode)  authCode: String?   include handler identity.signIn  include handler identity.identity} model AuthCode {  @id @autoIncrement @readonly  id: Int  @presentWithout(.phoneNumber) @unique  email: String?  @presentWithout(.email) @unique  phoneNumber: String?  @onSave($randomDigits(4))  code: String} middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

我们新增了一张AuthCode表,用来保存用户收到的验证码。我们用编程代码访问Teo的ORM API来进行验证码验证。代码如下,有三个语言版本,选择你采用的语言即可。

Node.js版本

import { App } from '@teocloud/teo'import { AuthCodeWhereUniqueInput, Teo, User } from './entities' const app = new App() app.mainNamespace().defineValidatorPipelineItem(    "validateAuthCode",     async (checkArgs: any, _, user: User, teo: Teo) => {        const finder: AuthCodeWhereUniqueInput = {}        if (checkArgs.ids.email) {            finder.email = user.email!        }        if (checkArgs.ids.phoneNumber) {            finder.phoneNumber = user.phoneNumber!        }        const authCode = await teo.authCode.findUnique({            where: finder        })        if (!authCode) {            return "auth code not found"        }        if (authCode.code !== checkArgs.value) {            return "auth code is wrong"        }}) app.run()

Python版本

from __future__ import annotationsfrom typing import Anyfrom asyncio import runfrom teo import Appfrom entities import Teo, User async def main():    app = App()    async def validate_auth_code(checker_args: dict[str, Any], _, user: User, teo: Teo):        finder = {}        if checker_args['ids'].get('email') is not None:            finder['email'] = user.email        if checker_args['ids'].get('phoneNumber') is not None:            finder['phoneNumber'] = user.phone_number        auth_code = await teo.auth_code.find_unique({            "where": finder        })        if auth_code is None:            return "auth code not found"        if auth_code.code != checker_args.value:            return "auth code is wrong"     app.main_namespace().define_validator_pipeline_item(        "validateAuthCode",         validate_auth_code) run(main())

Rust版本

pub mod entities; use entities::{Teo, User};use indexmap::indexmap;use tokio::main;use teo::prelude::{pipeline::item::validator::Validity, App, Result, Value, Error}; #[main]async fn main() -> Result<()> {    let app = App::new()?;    app.main_namespace_mut().define_validator_pipeline_item("validateAuthCode", move |check_args: Value, user: User, teo: Teo| async move {        let mut finder = Value::Dictionary(indexmap!{});        let check_args = check_args.as_dictionary().unwrap();        let ids = check_args.get("ids").unwrap().as_dictionary().unwrap();        if ids.contains_key("email") {            finder.as_dictionary_mut().unwrap().insert("email".to_owned(), ids.get("email").unwrap().clone());        }        if ids.contains_key("phoneNumber") {            finder.as_dictionary_mut().unwrap().insert("phoneNumber".to_owned(), ids.get("phoneNumber").unwrap().clone());        }        let auth_code = teo.auth_code().find_unique(finder).await?;        match auth_code {            Some(auth_code) => if auth_code.code().as_str() == check_args.get("value").unwrap().as_str().unwrap() {                Ok::<Validity, Error>(Validity::Valid)            } else {                Ok(Validity::Invalid("auth code is wrong".to_owned()))            },            None => Ok(Validity::Invalid("auth code not found".to_owned()))        }    });    app.run().await}

这三组代码做的事情是一样的,创建一个叫作"validateAuthCode"的pipeline item。在schema中,我们看到了这个pipeline item的位置,我们声明并使用了它。这个方法内部,我们根据用户输入的电话号码或邮箱地址,来查找一个验证码,一旦输入正确,我们给予用户登录,否则给予用户错误信息:验证码不存在或验证码错误。

举一反三

在刚刚展示的例子中,我们列举了常常容易被想到的token用法。如果实现一个人最多允许几个token,和invalid某个token,则需要单独建表并编写自定义的验证代码,像上面的验证码验证那样。其他的各种验证和token方式,也可以通过Teo强大的生命方式来实现。

声明式的魅力

在这逐步几次更新的schema中,我们看到了声明式编程的魅力。一切都那么简洁、易读、可描述。不再有难以看得懂的代码。因为声明式很紧凑,逻辑混乱的bug也很难出现。这样的代码,易于编写,易于部署。我们的开发者用户已经开始采用AI编程的方式来编写Teo服务器代码了。

开发文档

我们的官网内写有丰富的开发文档,支持白天模式和夜间模式的阅读。安装流程,快速开始指南,教程,概念,专题指南和API文档一应俱有。

官网:https://teocloud.io

支持我们

我们从2022年编写Teo至今,已经接近两年了,它从最初的仅支持Rust和MongoDB的框架,到现在支持三种语言,支持主流SQL和MongoDB,支持自定义handler和中间件,支持生成前端请求客户端,一路过来真的很辛苦。我们不断科研创新、探索研究,克服重重技术难题,把它做到现在。它是完全开源免费的框架。我们做它的目的,是让开发者能过上不卷、没有太大压力的好日子,让创业者和企业能够节省成本,更容易不被技术所负累,更容易成功。

我们十分需要您的支持,请关注我们的公众号,在Gitee为我们点一颗星。

Gitee: https://gitee.com/teocloud/teo

联系我们

在使用过程中,如果遇到任何困难,想要与我们沟通,或是提出功能需求,是非常容易的。带着点赞截图和公众号关注截图,添加我们的微信群群管微信caofz007,即可加入我们的用户群。

加入我们团队

目前我们都是以志愿者形式参与的开发,我是项目的发起者和主要编写者,框架核心,语言绑定,编辑器插件,网站都是我全职编写的。我们需要核心开发,前端开发,编辑器插件开发等人手,来把它做得更好。

我们现在有清晰的开发和推广,和商业化路线。在Teo具有一定规模和可见的良好未来的时候,即在具有一定底牌、资格、合适的时候,我们会积极努力寻找融资,把它做大。也会给予我们的项目贡献者,符合贡献价值的奖励。添加我们的伙伴caofz007这个微信,加入我们,成为我们的一部分。

展开阅读全文
点击加入讨论🔥(1) 发布并加入讨论🔥
本篇精彩评论
1 评论
4 收藏
分享
返回顶部
顶部