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 annotations
from typing import Any
from asyncio import run
from teo import App
from 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这个微信,加入我们,成为我们的一部分。