| @@ -106,7 +106,7 @@ GEM | |||
| activerecord (>= 3.1.0, < 7) | |||
| diff-lcs (1.3) | |||
| diffy (3.3.0) | |||
| doorkeeper (5.5.4) | |||
| doorkeeper (5.5.1) | |||
| railties (>= 5) | |||
| doorkeeper-jwt (0.4.1) | |||
| jwt (>= 2.1) | |||
| @@ -0,0 +1,42 @@ | |||
| class Projects::ProjectInviteLinksController < Projects::BaseController | |||
| before_action :require_manager!, except: [:show_link, :redirect_link] | |||
| before_action :require_login | |||
| def current_link | |||
| role = params[:role] | |||
| is_apply = params[:is_apply] | |||
| return render_error('请输入正确的参数!') unless role.present? && is_apply.present? | |||
| @project_invite_link = ProjectInviteLink.find_by(user_id: current_user.id, project_id: @project.id, role: role, is_apply: is_apply) | |||
| @project_invite_link = ProjectInviteLink.build!(@project, current_user, role, is_apply) unless @project_invite_link.present? | |||
| end | |||
| def generate_link | |||
| ActiveRecord::Base.transaction do | |||
| params_data = link_params.merge({user_id: current_user.id, project_id: @project.id}) | |||
| Projects::ProjectInviteLinks::CreateForm.new(params_data).validate! | |||
| @project_invite_link = ProjectInviteLink.build!(project, user, params_data[:role], params_data[:is_apply]) | |||
| end | |||
| rescue Exception => e | |||
| uid_logger_error(e.message) | |||
| tip_exception(e.message) | |||
| end | |||
| def show_link | |||
| @project_invite_link = ProjectInviteLink.find_by(sign: params[:invite_sign]) | |||
| return render_not_found unless @project_invite_link.present? | |||
| end | |||
| def redirect_link | |||
| Projects::LinkJoinService.call(current_user, @project, params[:invite_sign]) | |||
| render_ok | |||
| rescue Exception => e | |||
| uid_logger_error(e.message) | |||
| tip_exception(e.message) | |||
| end | |||
| private | |||
| def link_params | |||
| params.require(:project_invite_link).permit(:role, :is_apply) | |||
| end | |||
| end | |||
| @@ -1,4 +1,278 @@ | |||
| # Projects | |||
| ## 获取项目邀请链接(项目管理员) | |||
| 当前登录(管理员)用户获取项目邀请链接的接口(第一次请求会默认生成role类型为developer和is_apply为true的链接) | |||
| > 示例: | |||
| ```shell | |||
| curl -X GET http://localhost:3000/api/yystopf/kellect/project_invite_links/current_link.json | |||
| ``` | |||
| ```javascript | |||
| await octokit.request('GET /api/yystopf/kellect/project_invite_links/current_link.json') | |||
| ``` | |||
| ### HTTP 请求 | |||
| `GET /api/:owner/:repo/project_invite_links/current_link.json` | |||
| ### 请求参数 | |||
| 参数 | 必选 | 默认 | 类型 | 字段说明 | |||
| --------- | ------- | ------- | -------- | ---------- | |||
| |role |是| |string |项目权限,reporter: 报告者, developer: 开发者,manager:管理员 | | |||
| |is_apply |是| |boolean |是否需要审核 | | |||
| ### 返回字段说明 | |||
| 参数 | 类型 | 字段说明 | |||
| --------- | ----------- | ----------- | |||
| |id |int |链接id | | |||
| |role |string |邀请角色| | |||
| |is_apply |boolean |是否需要审核 | | |||
| |sign |string |邀请标识(放在链接后面即可)| | |||
| |expired_at |string |链接过期时间| | |||
| |user.id |int |链接创建者的id | | |||
| |user.type |string |链接创建者的类型 | | |||
| |user.name |string |链接创建者的名称 | | |||
| |user.login |string |链接创建者的标识 | | |||
| |user.image_url |string |链接创建者头像 | | |||
| |project.id |int |链接关联项目的id | | |||
| |project.identifier |string |链接关联项目的标识 | | |||
| |project.name |string |链接关联项目的名称 | | |||
| |project.description |string |链接关联项目的描述 | | |||
| |project.is_public |bool |链接关联项目是否公开 | | |||
| |project.owner.id |bool |链接关联项目拥有者id | | |||
| |project.owner.type |string |链接关联项目拥有者类型 | | |||
| |project.owner.name |string |链接关联项目拥有者昵称 | | |||
| |project.owner.login |string |链接关联项目拥有者标识 | | |||
| |project.owner.image_url|string |链接关联项目拥有者头像 | | |||
| > 返回的JSON示例: | |||
| ```json | |||
| { | |||
| "id": 7, | |||
| "role": "developer", | |||
| "is_apply": false, | |||
| "sign": "6b6b454843c291d4e52e60853cb8ad9f", | |||
| "expired_at": "2022-06-23 10:08", | |||
| "user": { | |||
| "id": 2, | |||
| "type": "User", | |||
| "name": "heh", | |||
| "login": "yystopf", | |||
| "image_url": "system/lets/letter_avatars/2/H/188_239_142/120.png" | |||
| }, | |||
| "project": { | |||
| "id": 474, | |||
| "identifier": "kellect", | |||
| "name": "kellect", | |||
| "description": null, | |||
| "is_public": true, | |||
| "owner": { | |||
| "id": 2, | |||
| "type": "User", | |||
| "name": "heh", | |||
| "login": "yystopf", | |||
| "image_url": "system/lets/letter_avatars/2/H/188_239_142/120.png" | |||
| } | |||
| } | |||
| } | |||
| ``` | |||
| ## 生成项目邀请链接(项目管理员) | |||
| 当前登录(管理员)用户生成的项目邀请链接,可选role和is_apply参数 | |||
| > 示例: | |||
| ```shell | |||
| curl -X POST http://localhost:3000/api/yystopf/kellect/project_invite_links/generate_link.json | |||
| ``` | |||
| ```javascript | |||
| await octokit.request('POST /api/yystopf/kellect/project_invite_links/generate_link.json') | |||
| ``` | |||
| ### HTTP 请求 | |||
| `POST /api/:owner/:repo/project_invite_links/generate_link.json` | |||
| ### 请求参数 | |||
| 参数 | 必选 | 默认 | 类型 | 字段说明 | |||
| --------- | ------- | ------- | -------- | ---------- | |||
| |role |是| |string |项目权限,reporter: 报告者, developer: 开发者,manager:管理员 | | |||
| |is_apply |是| |boolean |是否需要审核 | | |||
| > 请求的JSON示例 | |||
| ```json | |||
| { | |||
| "role": "developer", | |||
| "is_apply": false | |||
| } | |||
| ``` | |||
| ### 返回字段说明 | |||
| 参数 | 类型 | 字段说明 | |||
| --------- | ----------- | ----------- | |||
| |id |int |链接id | | |||
| |role |string |邀请角色| | |||
| |is_apply |boolean |是否需要审核 | | |||
| |sign |string |邀请标识(放在链接后面即可)| | |||
| |expired_at |string |链接过期时间| | |||
| |user.id |int |链接创建者的id | | |||
| |user.type |string |链接创建者的类型 | | |||
| |user.name |string |链接创建者的名称 | | |||
| |user.login |string |链接创建者的标识 | | |||
| |user.image_url |string |链接创建者头像 | | |||
| |project.id |int |链接关联项目的id | | |||
| |project.identifier |string |链接关联项目的标识 | | |||
| |project.name |string |链接关联项目的名称 | | |||
| |project.description |string |链接关联项目的描述 | | |||
| |project.is_public |bool |链接关联项目是否公开 | | |||
| |project.owner.id |bool |链接关联项目拥有者id | | |||
| |project.owner.type |string |链接关联项目拥有者类型 | | |||
| |project.owner.name |string |链接关联项目拥有者昵称 | | |||
| |project.owner.login |string |链接关联项目拥有者标识 | | |||
| |project.owner.image_url|string |链接关联项目拥有者头像 | | |||
| > 返回的JSON示例: | |||
| ```json | |||
| { | |||
| "id": 7, | |||
| "role": "developer", | |||
| "is_apply": false, | |||
| "sign": "6b6b454843c291d4e52e60853cb8ad9f", | |||
| "expired_at": "2022-06-23 10:08", | |||
| "user": { | |||
| "id": 2, | |||
| "type": "User", | |||
| "name": "heh", | |||
| "login": "yystopf", | |||
| "image_url": "system/lets/letter_avatars/2/H/188_239_142/120.png" | |||
| }, | |||
| "project": { | |||
| "id": 474, | |||
| "identifier": "kellect", | |||
| "name": "kellect", | |||
| "description": null, | |||
| "is_public": true, | |||
| "owner": { | |||
| "id": 2, | |||
| "type": "User", | |||
| "name": "heh", | |||
| "login": "yystopf", | |||
| "image_url": "system/lets/letter_avatars/2/H/188_239_142/120.png" | |||
| } | |||
| } | |||
| } | |||
| ``` | |||
| ## 获取邀请链接信息(被邀请用户) | |||
| 用户请求邀请链接时,通过该接口来获取链接的信息 | |||
| > 示例: | |||
| ```shell | |||
| curl -X GET http://localhost:3000/api/yystopf/kellect/project_invite_links/show_link.json?invite_sign=d612df03aad63760445c187bcf83f2e6 | |||
| ``` | |||
| ```javascript | |||
| await octokit.request('POST /api/yystopf/kellect/project_invite_links/show_link.json?invite_sign=d612df03aad63760445c187bcf83f2e6') | |||
| ``` | |||
| ### HTTP 请求 | |||
| `POST /api/:owner/:repo/project_invite_links/show_link.json?invite_sign=xxx` | |||
| ### 请求参数 | |||
| 参数 | 必选 | 默认 | 类型 | 字段说明 | |||
| --------- | ------- | ------- | -------- | ---------- | |||
| |invite_sign |是| |string |项目邀请链接的标识 | | |||
| ### 返回字段说明 | |||
| 参数 | 类型 | 字段说明 | |||
| --------- | ----------- | ----------- | |||
| |id |int |链接id | | |||
| |role |string |邀请角色| | |||
| |is_apply |boolean |是否需要审核 | | |||
| |sign |string |邀请标识(放在链接后面即可)| | |||
| |expired_at |string |链接过期时间| | |||
| |user.id |int |链接创建者的id | | |||
| |user.type |string |链接创建者的类型 | | |||
| |user.name |string |链接创建者的名称 | | |||
| |user.login |string |链接创建者的标识 | | |||
| |user.image_url |string |链接创建者头像 | | |||
| |project.id |int |链接关联项目的id | | |||
| |project.identifier |string |链接关联项目的标识 | | |||
| |project.name |string |链接关联项目的名称 | | |||
| |project.description |string |链接关联项目的描述 | | |||
| |project.is_public |bool |链接关联项目是否公开 | | |||
| |project.owner.id |bool |链接关联项目拥有者id | | |||
| |project.owner.type |string |链接关联项目拥有者类型 | | |||
| |project.owner.name |string |链接关联项目拥有者昵称 | | |||
| |project.owner.login |string |链接关联项目拥有者标识 | | |||
| |project.owner.image_url|string |链接关联项目拥有者头像 | | |||
| > 返回的JSON示例: | |||
| ```json | |||
| { | |||
| "id": 7, | |||
| "role": "developer", | |||
| "is_apply": false, | |||
| "sign": "6b6b454843c291d4e52e60853cb8ad9f", | |||
| "expired_at": "2022-06-23 10:08", | |||
| "user": { | |||
| "id": 2, | |||
| "type": "User", | |||
| "name": "heh", | |||
| "login": "yystopf", | |||
| "image_url": "system/lets/letter_avatars/2/H/188_239_142/120.png" | |||
| }, | |||
| "project": { | |||
| "id": 474, | |||
| "identifier": "kellect", | |||
| "name": "kellect", | |||
| "description": null, | |||
| "is_public": true, | |||
| "owner": { | |||
| "id": 2, | |||
| "type": "User", | |||
| "name": "heh", | |||
| "login": "yystopf", | |||
| "image_url": "system/lets/letter_avatars/2/H/188_239_142/120.png" | |||
| } | |||
| } | |||
| } | |||
| ``` | |||
| ## 接受项目邀请链接(被邀请用户) | |||
| 当前登录(非项目)用户加入项目的接口,如果项目链接不需要审核,请求成功后即加入项目,如果需要审核,那么会提交一个申请,需要项目管理员审核 | |||
| > 示例: | |||
| ```shell | |||
| curl -X POST http://localhost:3000/api/yystopf/kellect/project_invite_links/redirect_link.json?invite_sign=d612df03aad63760445c187bcf83f2e6 | |||
| ``` | |||
| ```javascript | |||
| await octokit.request('POST /api/yystopf/kellect/project_invite_links/redirect_link.json?invite_sign=d612df03aad63760445c187bcf83f2e6') | |||
| ``` | |||
| ### HTTP 请求 | |||
| `POST /api/:owner/:repo/project_invite_links/redirect_link.json?invite_sign=xxx` | |||
| ### 请求参数 | |||
| 参数 | 必选 | 默认 | 类型 | 字段说明 | |||
| --------- | ------- | ------- | -------- | ---------- | |||
| |invite_sign |是| |string |项目邀请链接的标识 | | |||
| > 返回的JSON示例: | |||
| ```json | |||
| { | |||
| "status": 0, | |||
| "message": "success" | |||
| } | |||
| ``` | |||
| ## 申请加入项目 | |||
| 申请加入项目 | |||
| @@ -0,0 +1,8 @@ | |||
| class Projects::ProjectInviteLinks::CreateForm < BaseForm | |||
| attr_accessor :user_id, :project_id, :role, :is_apply | |||
| validates :user_id, :project_id, :role, presence: true | |||
| validates :role, inclusion: { in: %w(manager developer reporter), message: "请输入正确的权限." } | |||
| validates :is_apply, inclusion: {in: [true, false], message: "请输入是否需要管理员审核."} | |||
| end | |||
| @@ -2,24 +2,27 @@ | |||
| # | |||
| # Table name: forge_applied_projects | |||
| # | |||
| # id :integer not null, primary key | |||
| # project_id :integer | |||
| # user_id :integer | |||
| # role :integer default("0") | |||
| # status :integer default("0") | |||
| # created_at :datetime not null | |||
| # updated_at :datetime not null | |||
| # id :integer not null, primary key | |||
| # project_id :integer | |||
| # user_id :integer | |||
| # role :integer default("0") | |||
| # status :integer default("0") | |||
| # created_at :datetime not null | |||
| # updated_at :datetime not null | |||
| # project_invite_link_id :integer | |||
| # | |||
| # Indexes | |||
| # | |||
| # index_forge_applied_projects_on_project_id (project_id) | |||
| # index_forge_applied_projects_on_user_id (user_id) | |||
| # index_forge_applied_projects_on_project_id (project_id) | |||
| # index_forge_applied_projects_on_project_invite_link_id (project_invite_link_id) | |||
| # index_forge_applied_projects_on_user_id (user_id) | |||
| # | |||
| class AppliedProject < ApplicationRecord | |||
| self.table_name = "forge_applied_projects" | |||
| belongs_to :user | |||
| belongs_to :project | |||
| belongs_to :project_invite_link, optional: true | |||
| has_many :applied_messages, as: :applied, dependent: :destroy | |||
| # has_many :forge_activities, as: :forge_act, dependent: :destroy | |||
| @@ -128,6 +128,7 @@ class Project < ApplicationRecord | |||
| has_many :pinned_projects, dependent: :destroy | |||
| has_many :has_pinned_users, through: :pinned_projects, source: :user | |||
| has_many :webhooks, class_name: "Gitea::Webhook", primary_key: :gpid, foreign_key: :repo_id | |||
| has_many :project_invite_links, dependent: :destroy | |||
| after_create :incre_user_statistic, :incre_platform_statistic | |||
| after_save :check_project_members | |||
| before_save :set_invite_code, :reset_unmember_followed, :set_recommend_and_is_pinned, :reset_cache_data | |||
| @@ -0,0 +1,59 @@ | |||
| # == Schema Information | |||
| # | |||
| # Table name: project_invite_links | |||
| # | |||
| # id :integer not null, primary key | |||
| # project_id :integer | |||
| # user_id :integer | |||
| # role :integer default("4") | |||
| # is_apply :boolean default("1") | |||
| # sign :string(255) | |||
| # expired_at :datetime | |||
| # created_at :datetime not null | |||
| # updated_at :datetime not null | |||
| # | |||
| # Indexes | |||
| # | |||
| # index_project_invite_links_on_project_id (project_id) | |||
| # index_project_invite_links_on_sign (sign) | |||
| # index_project_invite_links_on_user_id (user_id) | |||
| # | |||
| class ProjectInviteLink < ApplicationRecord | |||
| default_scope { where("expired_at > ?", Time.now).or(where(expired_at: nil)) } | |||
| belongs_to :project | |||
| belongs_to :user | |||
| has_many :applied_projects | |||
| scope :with_project_id, -> (project_id) {where(project_id: project_id)} | |||
| scope :with_user_id, -> (user_id) {where(user_id: user_id)} | |||
| enum role: {manager: 3, developer: 4, reporter: 5} | |||
| before_create :set_old_data_expired_at | |||
| def self.random_hex_sign | |||
| hex = (SecureRandom.hex(32)) | |||
| return hex unless ProjectInviteLink.where(sign: hex).exists? | |||
| end | |||
| def self.build!(project, user, role="developer", is_apply=true) | |||
| self.create!( | |||
| project_id: project&.id, | |||
| user_id: user&.id, | |||
| role: role, | |||
| is_apply: is_apply, | |||
| sign: random_hex_sign, | |||
| expired_at: Time.now + 3.days | |||
| ) | |||
| end | |||
| private | |||
| def set_old_data_expired_at | |||
| ProjectInviteLink.where(user_id: self.user_id, project_id: self.project, role: self.role, is_apply: self.is_apply).update_all(expired_at: Time.now) | |||
| end | |||
| end | |||
| @@ -0,0 +1,76 @@ | |||
| class Projects::LinkJoinService < ApplicationService | |||
| Error = Class.new(StandardError) | |||
| attr_reader :user, :project, :invite_sign, :params | |||
| def initialize(user, project, invite_sign, params={}) | |||
| @user = user | |||
| @project = project | |||
| @invite_sign = invite_sign | |||
| @params = params | |||
| end | |||
| def call | |||
| ActiveRecord::Base.transaction do | |||
| validate! | |||
| if invite_link.is_apply | |||
| # 如果需要申请才能加入,创建一条申请记录 | |||
| create_applied_project! | |||
| else | |||
| # 如果不需要申请,直接为项目添加该成员 | |||
| create_member! | |||
| end | |||
| end | |||
| end | |||
| private | |||
| def validate! | |||
| raise Error, 'invite_sign必须存在!' if invite_sign.blank? | |||
| raise Error, '邀请链接不存在!' unless invite_link.present? | |||
| raise Error, '邀请链接已失效!' unless invite_user_in_project | |||
| raise Error, '用户已加入该项目!' if project.member?(user.id) | |||
| end | |||
| def create_applied_project! | |||
| user.applied_projects.create!(project: project, role: role_value, project_invite_link_id: invite_link&.id) | |||
| end | |||
| def create_member! | |||
| Projects::AddMemberInteractor.call(project.owner, project, user, permission) | |||
| end | |||
| def invite_link | |||
| ProjectInviteLink.find_by(project_id: project.id, sign: invite_sign) | |||
| end | |||
| def invite_user_in_project | |||
| in_project = project.member?(invite_link.user) | |||
| invite_link.update_column(:expired_at, Time.now) unless in_project | |||
| in_project | |||
| end | |||
| def role_value | |||
| @_role ||= | |||
| case invite_link&.role | |||
| when 'manager' then 3 | |||
| when 'developer' then 4 | |||
| when 'reporter' then 5 | |||
| else | |||
| 5 | |||
| end | |||
| end | |||
| def permission | |||
| case invite_link&.role | |||
| when 'manager' | |||
| 'admin' | |||
| when 'developer' | |||
| 'write' | |||
| when 'reporter' | |||
| 'read' | |||
| else | |||
| 'read' | |||
| end | |||
| end | |||
| end | |||
| @@ -0,0 +1,8 @@ | |||
| json.id project.id | |||
| json.identifier project.identifier | |||
| json.name project.name | |||
| json.description project.description | |||
| json.is_public project.is_public | |||
| json.owner do | |||
| json.partial! "/users/user_simple", locals: {user: project.owner} | |||
| end | |||
| @@ -0,0 +1,12 @@ | |||
| json.(project_invite_link, :id, :role, :is_apply, :sign) | |||
| json.expired_at format_time(project_invite_link&.expired_at) | |||
| json.user do | |||
| json.partial! "/users/user_simple", locals: {user: project_invite_link.user} | |||
| end | |||
| if project_invite_link&.project.present? | |||
| json.project do | |||
| json.partial! "/projects/detail", locals: {project: project_invite_link.project} | |||
| end | |||
| else | |||
| json.project nil | |||
| end | |||
| @@ -0,0 +1 @@ | |||
| json.partial! 'detail', locals: { project_invite_link: @project_invite_link } | |||
| @@ -0,0 +1 @@ | |||
| json.partial! 'detail', locals: { project_invite_link: @project_invite_link } | |||
| @@ -0,0 +1 @@ | |||
| json.partial! 'detail', locals: { project_invite_link: @project_invite_link } | |||
| @@ -604,6 +604,14 @@ Rails.application.routes.draw do | |||
| post :cancel | |||
| end | |||
| end | |||
| resources :project_invite_links, only: [:index] do | |||
| collection do | |||
| get :current_link | |||
| post :generate_link | |||
| get :show_link | |||
| post :redirect_link | |||
| end | |||
| end | |||
| resources :webhooks, except: [:show, :new] do | |||
| member do | |||
| get :tasks | |||
| @@ -0,0 +1,16 @@ | |||
| class CreateProjectInviteLinks < ActiveRecord::Migration[5.2] | |||
| def change | |||
| create_table :project_invite_links do |t| | |||
| t.references :project | |||
| t.references :user | |||
| t.integer :role, default: 4 | |||
| t.boolean :is_apply, default: true | |||
| t.string :sign | |||
| t.datetime :expired_at | |||
| t.timestamps | |||
| end | |||
| add_index :project_invite_links, :sign | |||
| end | |||
| end | |||
| @@ -0,0 +1,6 @@ | |||
| class AddProjectInviteLinkToAppliedProjects < ActiveRecord::Migration[5.2] | |||
| def change | |||
| add_column :forge_applied_projects, :project_invite_link_id, :integer | |||
| add_index :forge_applied_projects, :project_invite_link_id | |||
| end | |||
| end | |||
| @@ -48,8 +48,8 @@ class CreateDoorkeeperTables < ActiveRecord::Migration[5.2] | |||
| # characters. More info on custom token generators in: | |||
| # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator | |||
| # | |||
| # t.text :token, null: false | |||
| t.string :token, null: false | |||
| t.text :token, null: false | |||
| # t.string :token, null: false | |||
| t.string :refresh_token | |||
| t.integer :expires_in | |||
| @@ -73,7 +73,7 @@ class CreateDoorkeeperTables < ActiveRecord::Migration[5.2] | |||
| t.string :previous_refresh_token, null: false, default: "" | |||
| end | |||
| add_index :oauth_access_tokens, :token, unique: true | |||
| # add_index :oauth_access_tokens, :token, unique: true | |||
| add_index :oauth_access_tokens, :refresh_token, unique: true | |||
| add_foreign_key( | |||
| :oauth_access_tokens, | |||
| @@ -1,5 +0,0 @@ | |||
| class ChangeOauthAccessTokensTokenColumnLength < ActiveRecord::Migration[5.2] | |||
| def change | |||
| change_column :oauth_access_tokens, :token, :string, limit: 500 | |||
| end | |||
| end | |||
| @@ -0,0 +1,5 @@ | |||
| require 'rails_helper' | |||
| RSpec.describe ProjectInviteLink, type: :model do | |||
| pending "add some examples to (or delete) #{__FILE__}" | |||
| end | |||