diff --git a/server/config-example.yaml b/server/config-example.yaml index ab0fa17..3901d7c 100644 --- a/server/config-example.yaml +++ b/server/config-example.yaml @@ -36,4 +36,11 @@ minio: secret_access_key: test123123 bucket: openkf app_bucket: openkf - location: us-east-1 \ No newline at end of file + location: us-east-1 + +email: + host: # smtp address + port: 25 + from: # email address + nickname: openkf + password: # smtp password \ No newline at end of file diff --git a/server/config.yaml b/server/config.yaml index ab0fa17..3901d7c 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -36,4 +36,11 @@ minio: secret_access_key: test123123 bucket: openkf app_bucket: openkf - location: us-east-1 \ No newline at end of file + location: us-east-1 + +email: + host: # smtp address + port: 25 + from: # email address + nickname: openkf + password: # smtp password \ No newline at end of file diff --git a/server/docs/docs.go b/server/docs/docs.go index 14f8173..e24d486 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -15,7 +15,76 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {} + "paths": { + "/api/v1/email/code": { + "post": { + "description": "Use email to send verification code", + "produces": [ + "application/json" + ], + "tags": [ + "mail" + ], + "summary": "SendCode", + "parameters": [ + { + "description": "Email address", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/param.SendToParams" + } + } + ], + "responses": { + "200": { + "description": "Send success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "param.SendToParams": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + } + } }` // SwaggerInfo holds exported Swagger Info so clients can modify it diff --git a/server/docs/swagger.json b/server/docs/swagger.json index ec416cd..920616d 100644 --- a/server/docs/swagger.json +++ b/server/docs/swagger.json @@ -3,5 +3,74 @@ "info": { "contact": {} }, - "paths": {} + "paths": { + "/api/v1/email/code": { + "post": { + "description": "Use email to send verification code", + "produces": [ + "application/json" + ], + "tags": [ + "mail" + ], + "summary": "SendCode", + "parameters": [ + { + "description": "Email address", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/param.SendToParams" + } + } + ], + "responses": { + "200": { + "description": "Send success", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "param.SendToParams": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + } + } } \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index b64379c..c40d2b3 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -1,4 +1,45 @@ +definitions: + param.SendToParams: + properties: + email: + type: string + required: + - email + type: object + response.Response: + properties: + code: + type: integer + data: {} + msg: + type: string + type: object info: contact: {} -paths: {} +paths: + /api/v1/email/code: + post: + description: Use email to send verification code + parameters: + - description: Email address + in: body + name: data + required: true + schema: + $ref: '#/definitions/param.SendToParams' + produces: + - application/json + responses: + "200": + description: Send success + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + summary: SendCode + tags: + - mail swagger: "2.0" diff --git a/server/go.mod b/server/go.mod index 67921de..e193e33 100644 --- a/server/go.mod +++ b/server/go.mod @@ -9,6 +9,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/minio/minio-go/v7 v7.0.57 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 diff --git a/server/go.sum b/server/go.sum index a6aa6b1..25f6a7e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -195,6 +195,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/server/internal/api/auth.go b/server/internal/api/auth.go new file mode 100644 index 0000000..b784ea7 --- /dev/null +++ b/server/internal/api/auth.go @@ -0,0 +1,5 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package api diff --git a/server/internal/api/auth/auth.go b/server/internal/api/auth/auth.go deleted file mode 100644 index 8832b06..0000000 --- a/server/internal/api/auth/auth.go +++ /dev/null @@ -1 +0,0 @@ -package auth diff --git a/server/internal/api/mail.go b/server/internal/api/mail.go new file mode 100644 index 0000000..908d260 --- /dev/null +++ b/server/internal/api/mail.go @@ -0,0 +1,41 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package api + +import ( + "github.com/OpenIMSDK/OpenKF/server/internal/common" + "github.com/OpenIMSDK/OpenKF/server/internal/common/response" + "github.com/OpenIMSDK/OpenKF/server/internal/param" + "github.com/OpenIMSDK/OpenKF/server/internal/service" + "github.com/OpenIMSDK/OpenKF/server/pkg/log" + "github.com/gin-gonic/gin" +) + +// SendCode +// @Tags mail +// @Summary SendCode +// @Description Use email to send verification code +// @Produce application/json +// @Param data body param.SendToParams true "Email address" +// @Success 200 {object} response.Response{msg=string} "Success" +// @Router /api/v1/email/code [post] +func SendCode(c *gin.Context) { + var params param.SendToParams + err := c.ShouldBindJSON(¶ms) + if err != nil { + response.FailWithCode(common.INVALID_PARAMS, c) + return + } + + svc := service.NewServiceWithGin(c) + err = svc.SendCode(params.Email) + if err != nil { + log.Debug("SendCode error: ", err) + response.FailWithCode(common.ERROR, c) + return + } + + response.Success(c) +} diff --git a/server/internal/client/mail.go b/server/internal/client/mail.go index 7fa59d5..e39d074 100644 --- a/server/internal/client/mail.go +++ b/server/internal/client/mail.go @@ -3,3 +3,54 @@ // you may not use this file except in compliance with the License. package client + +import ( + "fmt" + "net/smtp" + + "github.com/OpenIMSDK/OpenKF/server/internal/config" + "github.com/jordan-wright/email" +) + +// todo: use email connection pool to reduce the cost of creating a connection +// link: https://github.com/jordan-wright/email#a-pool-of-reusable-connections + +func SendEmail(to string, subject string, body string) error { + email := email.NewEmail() + + email.From = fmt.Sprintf("%s<%s>", config.Config.Email.Nickname, config.Config.Email.From) + email.To = []string{to} + email.Subject = subject + email.Text = []byte(body) + + if err := email.Send( + fmt.Sprintf("%s:%d", config.Config.Email.Host, config.Config.Email.Port), + smtp.PlainAuth( + "", config.Config.Email.From, config.Config.Email.Password, config.Config.Email.Host, + ), + ); err != nil { + return err + } + + return nil +} + +func SendHtmlEmail(to string, subject string, html string) error { + email := email.NewEmail() + + email.From = config.Config.Email.From + email.To = []string{to} + email.Subject = subject + email.HTML = []byte(html) + + if err := email.Send( + fmt.Sprintf("%s:%d", config.Config.Email.Host, config.Config.Email.Port), + smtp.PlainAuth( + "", config.Config.Email.From, config.Config.Email.Password, config.Config.Email.Host, + ), + ); err != nil { + return err + } + + return nil +} diff --git a/server/internal/client/minio.go b/server/internal/client/minio.go index c4dae51..9fb12d4 100644 --- a/server/internal/client/minio.go +++ b/server/internal/client/minio.go @@ -18,13 +18,12 @@ import ( var _minioClient *minio.Client var _bucket string -func init() { - endpoint := fmt.Sprintf("%s:%s", config.GetString("minio.ip"), - config.GetString("minio.port")) - accessKeyID := config.GetString("minio.access_key_id") - secretAccessKey := config.GetString("minio.secret_access_key") - location := config.GetString("minio.location") - _bucket = config.GetString("minio.bucket") +func InitMinio() { + endpoint := fmt.Sprintf("%s:%d", config.Config.Minio.Ip, config.Config.Minio.Port) + accessKeyID := config.Config.Minio.AccessKeyId + secretAccessKey := config.Config.Minio.SecretAccessKey + location := config.Config.Minio.Location + _bucket = config.Config.Minio.Bucket // Initialize _minioClient _minioClient, err := minio.New( diff --git a/server/internal/e/code.go b/server/internal/common/code.go similarity index 94% rename from server/internal/e/code.go rename to server/internal/common/code.go index eb71ccf..6327c37 100644 --- a/server/internal/e/code.go +++ b/server/internal/common/code.go @@ -2,7 +2,7 @@ // Licensed under the MIT License (the "License"); // you may not use this file except in compliance with the License. -package e +package common const ( SUCCESS = 200 diff --git a/server/internal/e/msg.go b/server/internal/common/msg.go similarity index 96% rename from server/internal/e/msg.go rename to server/internal/common/msg.go index 6d88283..a2908a7 100644 --- a/server/internal/e/msg.go +++ b/server/internal/common/msg.go @@ -2,7 +2,7 @@ // Licensed under the MIT License (the "License"); // you may not use this file except in compliance with the License. -package e +package common var Msg = map[int]string{ SUCCESS: "success", diff --git a/server/internal/common/response/response.go b/server/internal/common/response/response.go new file mode 100644 index 0000000..32a9020 --- /dev/null +++ b/server/internal/common/response/response.go @@ -0,0 +1,52 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package response + +import ( + "net/http" + + "github.com/OpenIMSDK/OpenKF/server/internal/common" + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +func NewResponse(code int, msg string, data interface{}, c *gin.Context) { + c.JSON(http.StatusOK, &Response{ + Code: code, + Msg: msg, + Data: data, + }) +} + +func Success(c *gin.Context) { + NewResponse(common.SUCCESS, common.GetMsg(common.SUCCESS), nil, c) +} +func SuccessWithData(data interface{}, c *gin.Context) { + NewResponse(common.SUCCESS, common.GetMsg(common.SUCCESS), data, c) +} +func SuccessWithCode(code int, c *gin.Context) { + NewResponse(code, common.GetMsg(code), nil, c) +} +func SuccessWithAll(code int, data interface{}, c *gin.Context) { + NewResponse(code, common.GetMsg(code), data, c) +} + +func Fail(c *gin.Context) { + NewResponse(common.ERROR, common.GetMsg(common.ERROR), nil, c) +} +func FailWithData(data interface{}, c *gin.Context) { + NewResponse(common.ERROR, common.GetMsg(common.ERROR), data, c) +} +func FailWithCode(code int, c *gin.Context) { + NewResponse(code, common.GetMsg(code), nil, c) +} +func FailWithAll(code int, data interface{}, c *gin.Context) { + NewResponse(code, common.GetMsg(code), data, c) +} diff --git a/server/internal/config/config.go b/server/internal/config/config.go index e17242e..826ded7 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -48,6 +48,13 @@ func ConfigInit(configPath string) { AppBucket: GetString("minio.app_bucket"), Location: GetString("minio.location"), }, + Email: Email{ + Host: GetString("email.host"), + Port: GetInt("email.port"), + From: GetString("email.from"), + Nickname: GetString("email.nickname"), + Password: GetString("email.password"), + }, } } @@ -58,6 +65,7 @@ type config struct { Mysql Mysql Redis Redis Minio Minio + Email Email } type App struct { @@ -101,3 +109,11 @@ type Minio struct { AppBucket string `mapstructure:"app_bucket"` Location string `mapstructure:"location"` } + +type Email struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + From string `mapstructure:"from"` + Nickname string `mapstructure:"nickname"` + Password string `mapstructure:"password"` +} diff --git a/server/internal/db/mysql.go b/server/internal/db/mysql.go index 1841a90..60f9ebe 100644 --- a/server/internal/db/mysql.go +++ b/server/internal/db/mysql.go @@ -15,7 +15,7 @@ import ( "gorm.io/gorm/logger" ) -var db *gorm.DB +var d *gorm.DB type Writer struct{} @@ -88,17 +88,18 @@ func InitMysqlDB() { db.Set("gorm:table_options", "CHARSET=utf8mb4") db.Set("gorm:table_options", "collation=utf8_unicode_ci") + d = db log.Info("Mysql", "connect ok", dsn) } // get mysql connection func GetMysqlDB() *gorm.DB { - return db + return d } func CloseMysqlDB() { - if db != nil { - sqlDB, err := db.DB() + if d != nil { + sqlDB, err := d.DB() if err != nil { log.Error("Mysql", err.Error(), " db.DB() failed ") } else { diff --git a/server/internal/db/redis.go b/server/internal/db/redis.go index 1a572de..3ca4e3c 100644 --- a/server/internal/db/redis.go +++ b/server/internal/db/redis.go @@ -16,7 +16,7 @@ import ( var r *redis.Client func InitRedisDB() { - r := redis.NewClient(&redis.Options{ + r = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", config.Config.Redis.Ip, config.Config.Redis.Port), Password: config.Config.Redis.Password, // no password set DB: config.Config.Redis.Database, // use default DB diff --git a/server/internal/param/mail.go b/server/internal/param/mail.go new file mode 100644 index 0000000..b57350e --- /dev/null +++ b/server/internal/param/mail.go @@ -0,0 +1,9 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package param + +type SendToParams struct { + Email string `json:"email" binding:"required"` +} diff --git a/server/internal/router/router.go b/server/internal/router/router.go index 96d66ec..7208e85 100644 --- a/server/internal/router/router.go +++ b/server/internal/router/router.go @@ -9,6 +9,7 @@ import ( swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" + "github.com/OpenIMSDK/OpenKF/server/internal/api" "github.com/OpenIMSDK/OpenKF/server/internal/config" "github.com/OpenIMSDK/OpenKF/server/internal/middleware" "github.com/gin-gonic/gin" @@ -27,7 +28,13 @@ func InitRouter() *gin.Engine { // swagger r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - r.Group("/api/v1") + apiv1 := r.Group("/api/v1") + { + email := apiv1.Group("/email") + { + email.POST("/code", api.SendCode) + } + } return r } diff --git a/server/internal/service/init.go b/server/internal/service/init.go new file mode 100644 index 0000000..a1696b4 --- /dev/null +++ b/server/internal/service/init.go @@ -0,0 +1,39 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package service + +import ( + "context" + + "github.com/OpenIMSDK/OpenKF/server/internal/db" + "github.com/gin-gonic/gin" + "github.com/go-redis/redis/v8" + "gorm.io/gorm" +) + +type Service struct { + // Context + Ctx context.Context + + // DB + Dao *gorm.DB + Cache *redis.Client +} + +func NewService() *Service { + return &Service{ + Ctx: context.Background(), + Dao: db.GetMysqlDB(), + Cache: db.GetRedis(), + } +} + +func NewServiceWithGin(c *gin.Context) *Service { + return &Service{ + Ctx: c.Request.Context(), + Dao: db.GetMysqlDB(), + Cache: db.GetRedis(), + } +} diff --git a/server/internal/service/mail.go b/server/internal/service/mail.go new file mode 100644 index 0000000..eeba33c --- /dev/null +++ b/server/internal/service/mail.go @@ -0,0 +1,31 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package service + +import ( + "time" + + "github.com/OpenIMSDK/OpenKF/server/internal/client" + "github.com/OpenIMSDK/OpenKF/server/internal/utils" +) + +func (svc *Service) SendCode(email string) (err error) { + // check the code is exist + cmd := svc.Cache.Get(svc.Ctx, "code:"+email) + var code string + if cmd.Err() != nil { + code = utils.GenerateCode() + // save code in 60s + status := svc.Cache.Set(svc.Ctx, "code:"+email, code, time.Second*60) + if status.Err() != nil { + return status.Err() + } + } + + // generate code + err = client.SendEmail(email, "OpenKF Admin Register", "Your verification code is "+code) + + return err +} diff --git a/server/internal/utils/code.go b/server/internal/utils/code.go new file mode 100644 index 0000000..328a694 --- /dev/null +++ b/server/internal/utils/code.go @@ -0,0 +1,23 @@ +// Copyright © 2023 OpenIMSDK open source community. All rights reserved. +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. + +package utils + +import ( + "math/rand" + "time" +) + +// Generate a random code with length 6 +func GenerateCode() string { + dataset := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()" + rand.Seed(time.Now().UnixNano()) + + code := make([]byte, 6) + for i := 0; i < 6; i++ { + code[i] = dataset[rand.Intn(len(dataset))] + } + + return string(code) +} diff --git a/server/main.go b/server/main.go index f7b409e..74130a6 100644 --- a/server/main.go +++ b/server/main.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" + "github.com/OpenIMSDK/OpenKF/server/internal/client" "github.com/OpenIMSDK/OpenKF/server/internal/config" "github.com/OpenIMSDK/OpenKF/server/internal/db" "github.com/OpenIMSDK/OpenKF/server/internal/router" @@ -27,6 +28,7 @@ func init() { log.InitLogger() db.InitMysqlDB() db.InitRedisDB() + client.InitMinio() } //go:generate go env -w GO111MODULE=on