diff --git a/README.md b/README.md index f034902..52981d6 100644 --- a/README.md +++ b/README.md @@ -1 +1,107 @@ -# sast-link-backend +# SAST Link + +[SAST Link Logo](https://aliyun.sastimg.mxte.cc/images/2023/07/02/footera9663bd5ff4b2bad.png) + +Logo designed by [SAST](https://sast.fun/), created by [Maxtune Lee](https://github.com/MaxtuneLee). + +[![Go Report Card](https://goreportcard.com/badge/github.com/NJUPT-SAST/sast-link-backend)](https://goreportcard.com/report/github.com/NJUPT-SAST/sast-link-backend) +[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](https://choosealicense.com/licenses/agpl-3.0/) + +SAST Link is a comprehensive personnel management system and OAuth designed to provide a secure and efficient way to manage and authorize access to your applications and services. + +Product design in Figma: [SAST Link](https://www.figma.com/file/IUIoRll3ieYFzJSfJPelDu/sast-link?node-id=0-1&t=rtc1sJfjJ0aTDAkp-0), designed by [Maxtune Lee](https://github.com/MaxtuneLee) + +This repository contains the backend code for SAST Link. If you're interested in the frontend, please visit [SAST Link frontend](https://github.com/NJUPT-SAST/sast-link). + +SAST Link backend is built with Go and PostgreSQL, and use gin as the web framework. + +> [!WARNING] +> This repo is under active development! Formats, schemas, and APIs are subject to rapid and backward incompatible changes! + +## Getting Started + +### Pre-requisites + +- Go +- PostgreSQL +- Redis +- Email Account (SMTP) +- Tencent COS (For file storage) +- Oauth2.0 Provider (e.g. GitHub, Feishu) + +Create PostgreSQL database and tables by running the SQL scripts in `sql/` directory. + +### Clone and Run + +To get started with SAST Link, follow these steps: + +1. Configuration: First, create a configuration file based on `config/dev-example.toml`. Ensure that you provide appropriate configurations for your environment. +2. Environment Setup: Set up the environment variable `CONFIG_FILE` to specify the configuration file you've created. +3. Installation and Execution: + +```bash +git clone https://github.com/NJUPT-SAST/sast-link-backend.git && cd sast-link-backend +CONFIG_FILE=dev-example go run . +``` + +The server will listen port `8080`, you can change it by add a `PORT` environment variable. + +## Development + +### API Documentation + +The API documentation is available at [wiki](https://github.com/NJUPT-SAST/sast-link-backend/wiki/Api-Doc) + +### Database Schema + +The database schema is available at [wiki](https://github.com/NJUPT-SAST/sast-link-backend/wiki/Project-Structure#sql) + +### Code Workflow Explanation + +The code workflow is available at [wiki](https://github.com/NJUPT-SAST/sast-link-backend/wiki/General) + +## Roadmap + +Goals and Vision for SAST Link (SAST OAuth and SAST Profile): + +**SAST OAuth:** + +SAST OAuth serves as a unified identity authentication system for SAST, facilitating login across multiple SAST applications. + +Example: + +- Simplifies login processes for SAST members across various projects, such as the FreshCup competition. +- Enables seamless login via SAST credentials without the need for separate accounts for each project. +- Allows SAST lecturers to access and manage the FreshCup competition system for tasks like grading via SAST login. +- Offers multiple login options including SAST Feishu, PassKey, QQ, Github, etc., providing users with convenience and flexibility. +- Implements additional security measures like F2A and security keys to enhance account security. + +In login process, users can choose to log in in multiple ways: SAST Feishu, PassKey, QQ, Github, etc. As long as they have been bound in advance, they can use third-party login, which is convenient and fast. They can also use F2A, security keys, and other methods to enhance account security. + +**SAST Profile:** + +SAST Profile acts as a centralized user profile system for managing user information and settings within SAST applications. + +Features: + +- Records basic user information such as SAST membership status, current position, department, group affiliation, etc. +- Tracks user activities within SAST, including competition results, awards, and permissions across various applications. +- Provides users with the ability to customize and share their profile page, allowing them to control the visibility of their information. + +**Current status**: + +- [x] User Management (Basic) +- [x] SAST OAuth (Basic) +- [x] File Storage (Tencent COS) +- [x] SAST Profile (Basic) +- [] SAST Link management +- [] Third-party OAuth (Github and Feishu now can be used in backend, but not fully implemente) + +## Contributing + +Pull requests and any feedback are welcome. For major changes, please open an issue first +to discuss what you would like to change. + +## License + +[AGPLv3 ](https://choosealicense.com/licenses/agpl-3.0/) diff --git a/api/v1/oauth_client_github.go b/api/v1/oauth_client_github.go index 2c1c84e..fb0d55e 100644 --- a/api/v1/oauth_client_github.go +++ b/api/v1/oauth_client_github.go @@ -37,9 +37,7 @@ func OauthGithubLogin(c *gin.Context) { oauthState := GenerateStateOauthCookie(c.Writer) url := githubConf.AuthCodeURL(oauthState) - log.Log.Println("------") - log.Log.Printf("Visit the URL for the auth dialog: %v\n", url) - log.Log.Println("------") + log.Log.Warnf("Visit the URL for the auth dialog: %v\n", url) c.Redirect(http.StatusFound, url) } diff --git a/api/v1/oauth_server.go b/api/v1/oauth_server.go index 9ff876c..e58e3cc 100644 --- a/api/v1/oauth_server.go +++ b/api/v1/oauth_server.go @@ -206,20 +206,7 @@ func OauthUserInfo(c *gin.Context) { func Authorize(c *gin.Context) { r := c.Request w := c.Writer - // store, err := session.Start(c, w, r) - // if err != nil { - // c.JSON(http.StatusInternalServerError, result.Failed(result.InternalErr.Wrap(err))) - // return - // } _ = r.ParseForm() - // var form url.Values - // if v, ok := store.Get("ReturnUri"); ok { - // form = v.(url.Values) - // } - // r.Form = form - // store.Delete("ReturnUri") - // _ = store.Save() - // Redirect user to login page if user not login or // Get code directly if user has logged in err := srv.HandleAuthorizeRequest(w, r) @@ -229,23 +216,6 @@ func Authorize(c *gin.Context) { } } -// User decides whether to authorize -// func UserAuth(c *gin.Context) { -// w := c.Writer -// r := c.Request -// -// //token := r.Header.Get("TOKEN") -// _ = r.ParseMultipartForm(0) -// token := c.PostForm("token") -// if token == "" { -// w.Header().Set("Content-Type", "application/json") -// response := result.Failed(result.AuthError) -// json, _ := json.Marshal(response) -// w.Write(json) -// return -// } -// } - // Get AccessToken func AccessToken(c *gin.Context) { w := c.Writer @@ -314,11 +284,6 @@ func getTokenByUUID(c context.Context, uuid string) (token string, err error) { } func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) { - // session, err := session.Start(r.Context(), w, r) - // if err != nil { - // return - // } - token := r.Form.Get("part") if token == "" { if r.Form == nil { @@ -327,9 +292,6 @@ func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string _ = r.ParseForm() } - // session.Set("ReturnUri", r.Form) - // _ = session.Save() - w.Header().Set("Content-Type", "application/json") response := result.Failed(result.TokenError) log.Log.Errorln("Oauth2 ::: token is empty") @@ -345,9 +307,6 @@ func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string _ = r.ParseForm() } - // session.Set("ReturnUri", r.Form) - // _ = session.Save() - w.Header().Set("Content-Type", "application/json") response := result.Failed(result.TokenError) log.Log.Errorln("Oauth2 ::: token is invalid") @@ -362,9 +321,6 @@ func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string _ = r.ParseForm() } - // session.Set("ReturnUri", r.Form) - // _ = session.Save() - w.Header().Set("Content-Type", "application/json") response := result.Failed(result.TokenError) log.Log.Errorln("Oauth2 ::: token is invalid") @@ -377,9 +333,6 @@ func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string _ = r.ParseForm() } - // session.Set("ReturnUri", r.Form) - // _ = session.Save() - w.Header().Set("Content-Type", "application/json") response := result.Failed(result.TokenError) log.Log.Errorln("Oauth2 ::: token is invalid") diff --git a/api/v1/user.go b/api/v1/user.go index cb31661..0a5d1c0 100644 --- a/api/v1/user.go +++ b/api/v1/user.go @@ -121,7 +121,6 @@ func SendEmail(ctx *gin.Context) { // 我开始乱写了啊啊啊啊 if usernameErr != nil { controllerLogger.Errorf("username parse error: %s", usernameErr.Error()) - //ctx.JSON(http.StatusUnauthorized, result.Failed(result.TicketNotCorrect)) ctx.JSON(http.StatusUnauthorized, result.Failed(result.HandleErrorWithArgu(usernameErr, result.TicketNotCorrect))) return } @@ -192,7 +191,6 @@ func Login(ctx *gin.Context) { // Get username from ticket username, err := util.GetUsername(ticket, model.LOGIN_TICKET_SUB) if err != nil || username == "" { - //ctx.JSON(http.StatusOK, result.Failed(result.TicketNotCorrect)) ctx.JSON(http.StatusOK, result.Failed(result.HandleErrorWithArgu(err, result.TicketNotCorrect))) return } @@ -200,8 +198,6 @@ func Login(ctx *gin.Context) { uid, err := service.Login(username, password) if err != nil { controllerLogger.Errorf("login fail: %s", err.Error()) - //ctx.JSON(http.StatusUnauthorized, result.Failed(result.VerifyAccountError)) - //ctx.AbortWithStatusJSON(http.StatusUnauthorized, result.Failed(result.VerifyPasswordError)) ctx.AbortWithStatusJSON(http.StatusUnauthorized, result.Failed(result.HandleErrorWithArgu(err, result.VerifyPasswordError))) return } diff --git a/config/config.go b/config/config.go index 28be880..15c0bc5 100644 --- a/config/config.go +++ b/config/config.go @@ -18,7 +18,7 @@ var Config *viper.Viper = viper.New() func init() { fileName := os.Getenv("CONFIG_FILE") if fileName == "" { - fileName = "dev-xun" + fileName = "dev-prod" } Config.AddConfigPath(".") Config.AddConfigPath("../../config") diff --git a/sql/admin.sql b/sql/admin.sql new file mode 100644 index 0000000..02673ee --- /dev/null +++ b/sql/admin.sql @@ -0,0 +1,12 @@ +-- public."admin" definition + +-- Drop table + +-- DROP TABLE public."admin"; + +CREATE TABLE public."admin" ( + id serial4 NOT NULL, + created_at timestamp NOT NULL DEFAULT now(), + user_id varchar(255) NOT NULL, + CONSTRAINT admin_pkey PRIMARY KEY (id) +); diff --git a/sql/carrer_records.sql b/sql/carrer_records.sql new file mode 100644 index 0000000..bd42ebc --- /dev/null +++ b/sql/carrer_records.sql @@ -0,0 +1,23 @@ +-- public.carrer_records definition + +-- Drop table + +-- DROP TABLE public.carrer_records; + +CREATE TABLE public.carrer_records ( + id serial4 NOT NULL, + user_id int4 NOT NULL, -- 与user表映射,表示某个用户的生涯记录 + org_id int2 NOT NULL, -- 与orgnize表映射,表示用户该届所在的组织 + grade int2 NOT NULL, -- 表示某一届(如:2023届) + is_delete bool NOT NULL, -- 假删 + "position" varchar(2) NULL, -- 包括:部员、讲师、组长、部长、主席 + CONSTRAINT carrer_records_pkey PRIMARY KEY (id) +); + +-- Column comments + +COMMENT ON COLUMN public.carrer_records.user_id IS '与user表映射,表示某个用户的生涯记录'; +COMMENT ON COLUMN public.carrer_records.org_id IS '与orgnize表映射,表示用户该届所在的组织'; +COMMENT ON COLUMN public.carrer_records.grade IS '表示某一届(如:2023届)'; +COMMENT ON COLUMN public.carrer_records.is_delete IS '假删'; +COMMENT ON COLUMN public.carrer_records."position" IS '包括:部员、讲师、组长、部长、主席'; diff --git a/sql/organize.sql b/sql/organize.sql new file mode 100644 index 0000000..3c1bdd3 --- /dev/null +++ b/sql/organize.sql @@ -0,0 +1,12 @@ +-- public.organize definition + +-- Drop table + +-- DROP TABLE public.organize; + +CREATE TABLE public.organize ( + id int4 NOT NULL DEFAULT nextval('department_id_seq'::regclass), + dep varchar(255) NOT NULL, + org varchar(255) NULL, + CONSTRAINT department_pkey PRIMARY KEY (id) +); diff --git a/sql/profile.sql b/sql/profile.sql new file mode 100644 index 0000000..c15214a --- /dev/null +++ b/sql/profile.sql @@ -0,0 +1,33 @@ +-- public.profile definition + +-- Drop table + +-- DROP TABLE public.profile; + +CREATE TABLE public.profile ( + id serial4 NOT NULL, + user_id int4 NOT NULL, -- 与user表映射 + nickname varchar(255) NOT NULL, -- 昵称 + org_id int2 NOT NULL, -- 对应部门和组的信息(现在的职位,历史职位的信息在carrer_records中) + bio varchar(255) NULL, -- 自我介绍 + email varchar(255) NOT NULL, -- 邮箱(默认展示) + badge json NULL, -- 纪念卡 + link _varchar NULL, -- 个人链接(包括自己b站、博客、GitHub等账号链接) + avatar varchar(255) NULL, -- 头像(存储oss链接) + is_deleted bool NOT NULL, -- 假删 + hide _varchar NULL, -- 选择隐藏的信息 + CONSTRAINT profile_pkey PRIMARY KEY (id) +); + +-- Column comments + +COMMENT ON COLUMN public.profile.user_id IS '与user表映射'; +COMMENT ON COLUMN public.profile.nickname IS '昵称'; +COMMENT ON COLUMN public.profile.org_id IS '对应部门和组的信息(现在的职位,历史职位的信息在carrer_records中)'; +COMMENT ON COLUMN public.profile.bio IS '自我介绍'; +COMMENT ON COLUMN public.profile.email IS '邮箱(默认展示)'; +COMMENT ON COLUMN public.profile.badge IS '纪念卡'; +COMMENT ON COLUMN public.profile.link IS '个人链接(包括自己b站、博客、GitHub等账号链接)'; +COMMENT ON COLUMN public.profile.avatar IS '头像(存储oss链接)'; +COMMENT ON COLUMN public.profile.is_deleted IS '假删'; +COMMENT ON COLUMN public.profile.hide IS '选择隐藏的信息'; diff --git a/sql/user.sql b/sql/user.sql new file mode 100644 index 0000000..9b0dfe6 --- /dev/null +++ b/sql/user.sql @@ -0,0 +1,19 @@ +-- public."user" definition + +-- Drop table + +-- DROP TABLE public."user"; + +CREATE TABLE public."user" ( + id serial4 NOT NULL, + created_at timestamp NOT NULL DEFAULT now(), + email varchar(255) NOT NULL, + uid varchar(255) NOT NULL, + qq_id varchar(255) NULL, + lark_id varchar(255) NULL, + github_id varchar(255) NULL, + wechat_id varchar(255) NULL, + is_deleted bool NOT NULL, + "password" varchar(255) NOT NULL, + CONSTRAINT user_pkey PRIMARY KEY (id) +);