当前位置: 首页 > news >正文

如何在go项目中实现发送邮箱验证码、邮箱+验证码登录

前期准备

  • GoLand :2024.1.1
    下载官网:https://www.jetbrains.com/zh-cn/go/download/other.html
    在这里插入图片描述
  • Postman:
    下载官网:https://www.postman.com/downloads/
    在这里插入图片描述

效果图(使用Postman)

  • Google
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • QQ
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

And so on…
Just you can try!


项目结构

本项目基于nunu基础上实现(github地址:https://github.com/go-nunu/nunu),Nunu是一个基于Golang的应用脚手架,它的名字来自于英雄联盟中的游戏角色,一个骑在雪怪肩膀上的小男孩。和努努一样,该项目也是站在巨人的肩膀上,它是由Golang生态中各种非常流行的库整合而成的,它们的组合可以帮助你快速构建一个高效、可靠的应用程序。拥有以下功能:
在这里插入图片描述

请添加图片描述

从nunu官方按照规范安装好之后:
在这里插入图片描述

基本操作流程

  1. 用户提交邮箱(email) 以请求 验证码(code)
  2. 服务器生成验证码并发送到用户邮箱。
  3. 用户输入收到的验证码和邮箱进行登录(login)
  4. 服务器验证验证码和邮箱。
  5. 如果验证成功,用户登录成功(sucess);否则,返回错误信息(error)

代码实现

1.internal/model/user.go和config/local.yml

注意:config和internal在同一级目录下

在这里插入图片描述

咱们先定义一个表结构,然后去连接数据库,创建对应映射的表,存储咱们的useridemail,验证码(code)是临时的,保存在cache里就好,不需要落库。

package modelimport ("time""gorm.io/gorm"
)type User struct {Id        string `gorm:"primarykey"`Email     string `gorm:"not null"`CreatedAt time.TimeUpdatedAt time.TimeDeletedAt gorm.DeletedAt `gorm:"index"`
}func (u *User) TableName() string {return "users"
}

建议直接从右边状态栏里直接连接mysql数据库:
在这里插入图片描述

对应的SQL建表语句:

create table users
(id         varchar(255) not nullprimary key,email      varchar(255) not null,created_at timestamp    not null,updated_at timestamp    not null,deleted_at timestamp    null,constraint emailunique (email),constraint idunique (id)
);

另外还需要在config包下修改local.yml数据库连接配置信息:

在这里插入图片描述在这里插入图片描述
库名为刚才所添加表的所在库名哦!

2.api/v1/user.go

package v1type LoginResponseData struct {AccessToken string `json:"accessToken"`
}type SendVerificationCodeRequest struct {Email string `json:"email"`
}type LoginByVerificationCodeRequest struct {Email string `json:"email"`Code  string `json:"code"`
}

这段Go代码定义了三个结构体:

  1. LoginResponseData:表示登录成功后的响应数据,包含一个AccessToken字段,用于标识用户的访问令牌。
  2. SendVerificationCodeRequest:表示发送验证代码请求的数据结构,包含一个Email字段,用于指定要发送验证代码的邮箱地址。
  3. LoginByVerificationCodeRequest:表示通过验证代码登录的请求数据结构,包含一个Email字段和一个Code字段,分别用于指定邮箱地址和收到的验证代码。

3.internal/repository/user.go

  • GetByEmail函数通过邮箱地址从数据库中获取用户信息。
  1. 参数:ctx context.Context表示上下文信息,email string表示要查询的邮箱地址。
  2. 返回值:*model.User表示查询到的用户信息,error表示错误信息。
  3. 该函数首先根据邮箱地址查询数据库中是否存在该用户,如果查询成功,则返回用户信息;如果查询失败,则返回错误信息。
  • CreateUserByEmail函数通过邮箱地址创建一个新的用户。
  1. 参数:ctx context.Context表示上下文信息,email string表示要创建的用户的邮箱地址。
  2. 返回值:*model.User表示创建的用户信息,error表示错误信息。
  3. 该函数首先生成一个唯一的用户ID,然后使用邮箱地址创建一个新的用户实例,并设置创建时间和更新时间为当前时间。
  4. 接着,将新用户实例插入到数据库中,如果插入成功,则返回新创建的用户信息;如果插入失败,则返回错误信息。
package repositoryimport ("context""errors""fmt""time""emerge-ai-core/common/utils""emerge-ai-core/internal/model""gorm.io/gorm"
)type UserRepository interface {GetByEmail(ctx context.Context, email string) (*model.User, error)CreateUserByEmail(ctx context.Context, email string) (*model.User, error)
}func NewUserRepository(r *Repository,
) UserRepository {return &userRepository{Repository: r,}
}type userRepository struct {*Repository
}func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {var user model.Userif err := r.DB(ctx).Where("email = ?", email).First(&user).Error; err != nil {return nil, err}return &user, nil
}// CreateUserByEmail creates a user by email
func (r *userRepository) CreateUserByEmail(ctx context.Context, email string) (*model.User, error) {now := time.Now()user := &model.User{Id:        utils.GenerateUUID(),Email:     email,CreatedAt: now,UpdatedAt: now,}if err := r.DB(ctx).Create(user).Error; err != nil {return nil, fmt.Errorf("failed to create user by email: %v", err)}return user, nil
}

4.internal/service/email.go和internal/service/user.go

在这里插入图片描述

user.go

  • 定义了一个名为UserService的接口,其中包含一个GenerateTokenByUserEmail方法,用于生成用户的令牌。实现该接口的是userService结构体,它通过NewUserService函数进行实例化。GenerateTokenByUserEmail方法首先通过userRepo获取用户信息,如果用户不存在,则创建新用户,并使用jwt.GenToken方法生成令牌。
package serviceimport ("context""errors""time"v1 "emerge-ai-core/api/v1""emerge-ai-core/internal/model""emerge-ai-core/internal/repository""github.com/patrickmn/go-cache""golang.org/x/crypto/bcrypt""gorm.io/gorm"
)type UserService interface {GenerateTokenByUserEmail(ctx context.Context, email string) (string, error)
}func NewUserService(service *Service,userRepo repository.UserRepository,
) UserService {return &userService{userRepo: userRepo,Service:  service,}
}type userService struct {userRepo     repository.UserRepositoryemailService EmailService*Service
}// GenerateTokenByUserEmail generates a token for a user
func (s *userService) GenerateTokenByUserEmail(ctx context.Context, email string) (string, error) {// get user by emailuser, err := s.userRepo.GetByEmail(ctx, email)if err != nil {if errors.Is(err, gorm.ErrRecordNotFound) {// is new user create useruser, err = s.userRepo.CreateUserByEmail(ctx, email)if err != nil {return "", err}} else {return "", err}}// generate tokentoken, err := s.jwt.GenToken(user.Id, time.Now().Add(time.Hour*24*1))if err != nil {return "", err}return token, nil
}

email.go

  • 提供了一个电子邮件服务,用于发送和验证用户邮箱中的验证代码。
package serviceimport ("context""fmt""math/rand""net/smtp""time""github.com/jordan-wright/email""github.com/patrickmn/go-cache"
)var (// cache for storing verification codes// 缓存中的验证代码将在创建后5分钟内有效,且每隔10分钟进行一次清理。verificationCodeCache = cache.New(5*time.Minute, 10*time.Minute)
)type EmailService interface {SendVerificationCode(ctx context.Context, to string) errorVerifyVerificationCode(email string, code string) bool
}type emailService struct {
}func NewEmailService() EmailService {return &emailService{}
}// SendVerificationCode sends a verification code to the user's email
func (e *emailService) SendVerificationCode(ctx context.Context, to string) error {code := generateVerificationCode()err := e.sendVerificationCode(to, code)if err != nil {return err}// store the verification code in the cache for later verificationverificationCodeCache.Set(to, code, cache.DefaultExpiration)return nil
}// sendVerificationCode 发送验证代码到指定的邮箱。
// 参数 to: 邮件接收人的邮箱地址。
// 参数 code: 需要发送的验证代码。
// 返回值 error: 发送过程中遇到的任何错误。
func (e *emailService) sendVerificationCode(to string, code string) error {// 创建一个新的邮件实例em := email.NewEmail()em.From = "Xxxxxxx <xxxxxxxxxx@qq.com>"em.To = []string{to}em.Subject = "Verification Code"// 设置邮件的HTML内容em.HTML = []byte(`<h1>Verification Code</h1><p>Your verification code is: <strong>` + code + `</strong></p>`)// 发送邮件(这里使用QQ进行发送邮件验证码)err := em.Send("smtp.qq.com:587", smtp.PlainAuth("", "xxxxxxxxxx@qq.com", "这里填写的是授权码", "smtp.qq.com"))if err != nil {return err // 如果发送过程中有错误,返回错误信息}return nil // 邮件发送成功,返回nil
}// 随机生成一个6位数的验证码。
func generateVerificationCode() string {rand.Seed(time.Now().UnixNano())code := fmt.Sprintf("%06d", rand.Intn(1000000))return code
}// VerifyVerificationCode verifies the verification code sent to the user
func (e *emailService) VerifyVerificationCode(email string, code string) bool {// debug codeif code == "123456" {return true}// retrieve the verification code from the cachecachedCode, found := verificationCodeCache.Get(email)// 如果没有找到验证码或者验证码过期,返回falseif !found {return false}// compare the cached code with the provided codeif cachedCode != code {return false}return true
}

注意:这里需要SMTP协议知识,并且要想获取到授权码,一般要去所在邮箱官方进行申请,这里以QQ为例:

  1. 电脑端打开QQ邮箱,点击设置
    在这里插入图片描述

  2. 点击账号在这里插入图片描述

  3. 往下滑,找到POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,我这里已经开启了服务。
    在这里插入图片描述
    在这里插入图片描述

  4. 即可获取到授权码!

5.internal/handler/user.go

  • 处理用户通过验证代码登录的HTTP请求
package handlerimport ("net/http""emerge-ai-core/api/v1""emerge-ai-core/internal/model""emerge-ai-core/internal/service""github.com/gin-gonic/gin""go.uber.org/zap"
)type UserHandler struct {*HandleruserService  service.UserServiceemailService service.EmailService
}func NewUserHandler(handler *Handler, userService service.UserService, emailService service.EmailService) *UserHandler {return &UserHandler{Handler:      handler,userService:  userService,emailService: emailService,}
}// SendVerificationCode send verification code
func (h *UserHandler) SendVerificationCode(ctx *gin.Context) {var req v1.SendVerificationCodeRequestif err := ctx.ShouldBindJSON(&req); err != nil {v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())return}if err := h.emailService.SendVerificationCode(ctx, req.Email); err != nil {v1.HandleError(ctx, http.StatusInternalServerError, v1.ErrInternalServerError, err.Error())return}v1.HandleSuccess(ctx, nil)
}// LoginByVerificationCode by verification code
func (h *UserHandler) LoginByVerificationCode(ctx *gin.Context) {var req v1.LoginByVerificationCodeRequestif err := ctx.ShouldBindJSON(&req); err != nil {v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())return}// check verification codeif !h.emailService.VerifyVerificationCode(req.Email, req.Code) {v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil)return}token, err := h.userService.GenerateTokenByUserEmail(ctx, req.Email)if err != nil {v1.HandleError(ctx, http.StatusUnauthorized, v1.ErrUnauthorized, err.Error())return}v1.HandleSuccess(ctx, v1.LoginResponseData{AccessToken: token,})
}

6.internal/server/http.go

  • 创建一个以/v1为前缀的路由分组v1,然后在该分组下创建子分组/public。在/public子分组下定义了两个POST请求的路由,分别对应/send-verification-code/login,并绑定相应的处理函数。
package serverimport (apiV1 "emerge-ai-core/api/v1""emerge-ai-core/docs""emerge-ai-core/internal/handler""emerge-ai-core/internal/middleware""emerge-ai-core/pkg/jwt""emerge-ai-core/pkg/log""emerge-ai-core/pkg/server/http""github.com/gin-gonic/gin""github.com/spf13/viper"swaggerfiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger"
)func NewHTTPServer(logger *log.Logger,conf *viper.Viper,jwt *jwt.JWT,userHandler *handler.UserHandler,chatHandler *handler.ChatHandler,
) *http.Server {gin.SetMode(gin.DebugMode)s := http.NewServer(gin.Default(),logger,http.WithServerHost(conf.GetString("http.host")),http.WithServerPort(conf.GetInt("http.port")),)...v1 := s.Group("/v1"){publicRouter := v1.Group("/public"){// POST /v1/public/send-verification-codepublicRouter.POST("/send-verification-code", userHandler.SendVerificationCode)// POST /v1/public/loginpublicRouter.POST("/login", userHandler.LoginByVerificationCode)}}return s
}

Postman测试

同效果图

  • Google
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • QQ
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

And so on…
Just you can try!

相关文章:

如何在go项目中实现发送邮箱验证码、邮箱+验证码登录

前期准备 GoLand &#xff1a;2024.1.1 下载官网&#xff1a;https://www.jetbrains.com/zh-cn/go/download/other.html Postman&#xff1a; 下载官网&#xff1a;https://www.postman.com/downloads/ 效果图(使用Postman) Google&#xff1a; QQ&#xff1a; And …...

Docker 部署 Nginx 实现一个极简的 负载均衡

背景: Nginx是异步框架的网页服务器&#xff0c;其常用作反向代理(负载均衡器)。在一般的小项目中, 服务器不多, 如果不考虑使用服务注册与发现, 使用Nginx 可以容易实现负载均衡。 在特此写一个快速入门 Nginx 的技术贴, 使用 Docker 部署 Nginx, 实现一个极简的加权轮询负载均…...

Java刷题总结(面试)

1、String类 String不可变 java 中String是 immutable的&#xff0c;也就是不可变&#xff0c;一旦初始化&#xff0c;其引用指向的内容是不可变的。 也就是说&#xff0c;String str “aa”&#xff1b;str“bb”&#xff1b;第二句不是改变“aa”所存储地址的内容&#xf…...

ipad air6电容笔推荐,2024十大高性价比电容笔排行榜!

​电容笔作为ipad的最佳拍档&#xff0c;为学生党和打工人带来了极大的便利&#xff0c;二者搭配效率真的大大提升&#xff0c;但是&#xff0c;如何选购一支适合自己的电容笔呢&#xff1f;作为一个对数码设备非常感兴趣并且有一定了解的人&#xff0c;我根据自己多年的使用经…...

Java Memorandum

Java Memorandum 1 定义安全的集合2 collection集合用迭代器删除元素时避免并发修改异常3 异常捕获4 RequestBody和RequestParam和Parameter区别4.1 RequestBody4.2 RequestParam4.3 Parameter 1 定义安全的集合 void old() {ArrayList<Apple> apples new ArrayList<…...

大数据学习之 Hadoop部署

Hadoop部署 Linux桌面模式关闭 # 设置 systemctl set-default multi-user.target # 重启 reboot防火墙关闭 systemctl status firewalld systemctl stop firewalld # 关闭开机自启 systemctl disable firewalld配置Java环境 echo $JAVA_HOME java -version # Java配置 # 上传ja…...

xxe漏洞--xml外部实体注入漏洞

1.xxe漏洞介绍 XXE&#xff08;XML External Entity Injection&#xff09;是一种攻击技术&#xff0c;它允许攻击者注入恶意的外部实体到XML文档中。如果应用程序处理XML输入时未正确配置&#xff0c;攻击者可以利用这个漏洞访问受影响系统上的敏感文件、执行远程代码、探测内…...

Nginx反向代理与负载均衡:让网站像海豚一样灵活

引言&#xff1a;"当网站遇上海豚&#xff1a;Nginx让数据流动更流畅&#xff01;"想象一下&#xff0c;你的网站是一片繁忙的海域&#xff0c;而Nginx就像一群聪明的海豚&#xff0c;它们不仅能够迅速地找到最佳的捕食路线&#xff08;反向代理&#xff09;&#xf…...

企业应考虑的优秀云安全措施

作为云客户&#xff0c;企业有责任确保正确使用他们提供的工具来保证数据和应用程序的安全。让德迅云安全来跟大家一起研究一些典型企业应该考虑的优秀云安全措施。 在数据安全和隐私方面&#xff0c;企业是否在努力跟上疫情的发展?企业不是一个人。就像多年以前&#xff0c;C…...

如何将老板的游戏机接入阿里云自建K8S跑大模型(下)- 安装nvidia/gpu-operator支持GPU在容器中共享

文章目录 安装nvidia/gpu-operator支持GPU在容器中共享 安装nvidia/gpu-operator支持GPU在容器中共享 安装 nvidia/gpu-operator遇到两个问题&#xff1a; 由于我们都懂的某个原因&#xff0c;导致某些镜像一直现在不成功。 解决办法&#xff0c;准备一个&#x1fa9c;&#…...

代码随想录-Day16

104. 二叉树的最大深度 方法一&#xff1a;深度优先搜索 class Solution {public int maxDepth(TreeNode root) {if (root null) {return 0;} else {int leftHeight maxDepth(root.left);int rightHeight maxDepth(root.right);return Math.max(leftHeight, rightHeight) …...

31.@Anonymous

1►@Anonymous原理 大家应该已经习惯我的教学套路,很多时候都是先使用,然后讲述原理。 上节课我们使用了注解@Anonymous,然后接口就可以直接被访问到了,不用token!不用token!不用token!。 我们一般知道,注解是给程序看的,给机器看的,当然也是给程序员看的。注解如果…...

oracle 表同一列只取最新一条数据写法

select * from (select t.*,row_number() over(partition by 去重列名 order by 排序列名 desc) as rnfrom 表名)where rn1 1.row_number() over(....): 为每条数据分配一个行号,1.2.3....这样的 2.partition by : 以某列作为分组&#xff0c;每个分组行号从1开始&#xf…...

C语言游戏实战(12):植物大战僵尸(坤版)

植物大战僵尸 前言&#xff1a; 本游戏使用C语言和easyx图形库编写&#xff0c;通过这个项目我们可以深度的掌握C语言的各种语言特性和高级开发技巧&#xff0c;以及锻炼我们独立的项目开发能力&#xff0c; 在开始编写代码之前&#xff0c;我们需要先了解一下游戏的基本规则…...

提权方式及原理汇总

一、Linux提权 1、SUID提权 SUID&#xff08;设置用户ID&#xff09;是赋予文件的一种权限&#xff0c;它会出现在文件拥有者权限的执行位上&#xff0c;具有这种权限的文件会在其执行时&#xff0c;使调用者暂时获得该文件拥有者的权限。 为可执行文件添加suid权限的目的是简…...

【leetcode----二叉树中的最大路径和】

二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点&#xff0c;且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root &#xff0c…...

Rust: 编译过程中链接器 `cc` 没有找到

这个错误信息表明在编译过程中链接器 cc 没有找到。cc 通常是 C 编译器的符号链接&#xff0c;它指向系统上的实际 C 编译器&#xff0c;如 gcc 或 clang。这个错误通常意味着你的系统缺少必要的编译工具链。 要解决这个问题&#xff0c;你需要确保你的系统上安装了 C 编译器。…...

【vue-3】动态属性绑定v-bind

1、文本动态绑定&#xff1a; <input type"text" v-bind:value"web.url"> 简写&#xff1a; <input type"text" :value"web.url"> 2、文字样式动态绑定 <b :class"{textColor:web.fontStatus}">vue学…...

Rust:多线程环境下使用 Mutex<T> 还是 Arc<Mutex<T>> ?

在 Rust 中&#xff0c;Mutex 本身不是线程不安全的&#xff1b;它提供了内部的线程同步机制。然而&#xff0c;如果你想在多线程环境中共享同一个 Mutex&#xff0c;你需要确保这个 Mutex 可以被多个线程访问。为此&#xff0c;你通常需要使用 Arc<Mutex<T>>。Arc…...

关于如何创建一个可配置的 SpringBoot Web 项目的全局异常处理

前情概要 这个问题其实困扰了我一周时间&#xff0c;一周都在 Google 上旅游&#xff0c;我要如何动态的设置 RestControllerAdvice 里面的 basePackages 以及 baseClasses 的值呢&#xff1f;经过一周的时间寻求无果之后打算决定放弃的我终于找到了一些关键的线索。 当然在此…...

docker三种自定义网络(虚拟网络) overlay实现原理

docker提供了三种自定义网络驱动&#xff1a;bridge、overlay、macvlan。 bridge驱动类似默认的bridge网络模式。 overlay和macvlan是用于创建跨主机网络。 支持自定义网段、网关&#xff0c;docker network create --subnet 172.77.0.0/24 --gateway 172.77.0.1 my_n…...

C#上位机1ms级高精度定时任务

precisiontimer 安装扩展包 添加引用 完整代码 using PrecisionTiming;using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; us…...

盘点28个免费域名申请大全

盘点28个免费域名申请大全 免费域名推荐学习使用&#xff0c;免费就意味着没任何保障。 名称稳定时间支持解析模式后缀格式说明地址EU.org28 年NS.eu.org/. 国家简写.eu.org需要审核&#xff0c;稳定性高&#xff0c;限制少&#xff0c;国内访问有问题&#xff0c;可 CFeu.orgp…...

【vue】封装的天气展示卡片,在线获取天气信息

源码 <template><div class"sen_weather_wrapper"><div class"sen_top_box"><div class"sen_left_box"><div class"sen_top"><div class"sen_city">山东</div><qctc-time cl…...

【MySQL】库的操作和表的操作

库的操作和表的操作 一、库的操作1、创建数据库(create)2、字符集和校验规则&#xff08;1&#xff09;查看系统默认字符集以及校验规则&#xff08;2&#xff09;查看数据库支持的字符集&#xff08;3&#xff09;查看数据库支持的字符集校验规则&#xff08;4&#xff09;校验…...

【学习笔记】后端(Ⅰ)—— NodeJS(Ⅱ)

NodeJS 3、进阶篇 —— Express框架 3.1、Express 框架介绍 3.2、Express 框架初体验 3.3、使用 3.4、中间件 3.5、托管静态文件 3.6、获取表单数据 3.7、防盗链 3.8、路由模式化 3.8、EJS 模板引擎 3.9、express-generator…...

VMware报平台不支持虚拟化Win10家庭版关闭Hyper-V及内核隔离

1.BIOS中开启虚拟化功能 2.启动或关闭程序中找不到Hyper-v 停止 hypervisorlaunchtype&#xff08;Windows Hyper-V 启动加载器&#xff09; 以管理员的身份打开命令行窗口&#xff0c;运行如下命令&#xff0c;关闭停止 Windows Hyper-V 启动加载器 开启 Windows Hyper-V 启…...

简单介绍十款可以免费使用的API测试工具

API开发应该是后端开发最常见的工作&#xff0c;而调试和测试API是非常关键的&#xff0c;这篇文章简单介绍几款常用的工具以供大家参考。 SoapUI SoapUI是很老牌的工具的&#xff0c;在之前Webservice盛行的时候经常会用到。 现在官方推出了Pro版本的ReadyAPI&#xff0c;但要…...

非授权人员进入报警系统

非授权人员进入报警系统基于智能视频分析技术和深度学习技术&#xff0c;非授权人员进入报警系统通过现场已经装好的监控摄像头针对人体进行精准检测&#xff0c;并根据设置的禁入区范围进行判断。通过图像处理和人体识别算法&#xff0c;非授权人员进入报警系统可以在实时监测…...

Mysql基础教程(03):AND

MySQL AND 运算符的用法 本文介绍了 MySQL 中如何在 WHERE 子句中使用 AND 运算符组合多个查询条件过滤查询数据。 当使用 SELECT 查询数据时&#xff0c;如果 WHERE 子句中有多个条件&#xff0c;可以根据需要使用 AND, OR, 或者 NOT 运算符将他们组合起来。本文主要介绍 AN…...