Browse Source

Merge remote-tracking branch 'origin/develop' into develop

pull/313/head
xiaoxiaoqiong 4 years ago
parent
commit
c426467a68
20 changed files with 1319 additions and 226 deletions
  1. +1
    -1
      Gemfile.lock
  2. +42
    -0
      app/controllers/projects/project_invite_links_controller.rb
  3. +274
    -0
      app/docs/slate/source/includes/_projects.md
  4. +8
    -0
      app/forms/projects/project_invite_links/create_form.rb
  5. +12
    -9
      app/models/applied_project.rb
  6. +1
    -0
      app/models/project.rb
  7. +59
    -0
      app/models/project_invite_link.rb
  8. +76
    -0
      app/services/projects/link_join_service.rb
  9. +8
    -0
      app/views/projects/_detail.json.jbuilder
  10. +12
    -0
      app/views/projects/project_invite_links/_detail.json.jbuilder
  11. +1
    -0
      app/views/projects/project_invite_links/current_link.json.jbuilder
  12. +1
    -0
      app/views/projects/project_invite_links/generate_link.json.jbuilder
  13. +1
    -0
      app/views/projects/project_invite_links/show_link.json.jbuilder
  14. +8
    -0
      config/routes.rb
  15. +16
    -0
      db/migrate/20220614070028_create_project_invite_links.rb
  16. +6
    -0
      db/migrate/20220614081950_add_project_invite_link_to_applied_projects.rb
  17. +3
    -3
      db/migrate/20220616040105_create_doorkeeper_tables.rb
  18. +0
    -5
      db/migrate/20220617103002_change_oauth_access_tokens_token_column_length.rb
  19. +785
    -208
      public/docs/api.html
  20. +5
    -0
      spec/models/project_invite_link_spec.rb

+ 1
- 1
Gemfile.lock View File

@@ -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)


+ 42
- 0
app/controllers/projects/project_invite_links_controller.rb View File

@@ -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

+ 274
- 0
app/docs/slate/source/includes/_projects.md View File

@@ -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"
}
```

## 申请加入项目
申请加入项目


+ 8
- 0
app/forms/projects/project_invite_links/create_form.rb View File

@@ -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

+ 12
- 9
app/models/applied_project.rb View File

@@ -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


+ 1
- 0
app/models/project.rb View File

@@ -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


+ 59
- 0
app/models/project_invite_link.rb View File

@@ -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

+ 76
- 0
app/services/projects/link_join_service.rb View File

@@ -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

+ 8
- 0
app/views/projects/_detail.json.jbuilder View File

@@ -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

+ 12
- 0
app/views/projects/project_invite_links/_detail.json.jbuilder View File

@@ -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

+ 1
- 0
app/views/projects/project_invite_links/current_link.json.jbuilder View File

@@ -0,0 +1 @@
json.partial! 'detail', locals: { project_invite_link: @project_invite_link }

+ 1
- 0
app/views/projects/project_invite_links/generate_link.json.jbuilder View File

@@ -0,0 +1 @@
json.partial! 'detail', locals: { project_invite_link: @project_invite_link }

+ 1
- 0
app/views/projects/project_invite_links/show_link.json.jbuilder View File

@@ -0,0 +1 @@
json.partial! 'detail', locals: { project_invite_link: @project_invite_link }

+ 8
- 0
config/routes.rb View File

@@ -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


+ 16
- 0
db/migrate/20220614070028_create_project_invite_links.rb View File

@@ -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

+ 6
- 0
db/migrate/20220614081950_add_project_invite_link_to_applied_projects.rb View File

@@ -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

+ 3
- 3
db/migrate/20220616040105_create_doorkeeper_tables.rb View File

@@ -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,


+ 0
- 5
db/migrate/20220617103002_change_oauth_access_tokens_token_column_length.rb View File

@@ -1,5 +0,0 @@
class ChangeOauthAccessTokensTokenColumnLength < ActiveRecord::Migration[5.2]
def change
change_column :oauth_access_tokens, :token, :string, limit: 500
end
end

+ 785
- 208
public/docs/api.html
File diff suppressed because it is too large
View File


+ 5
- 0
spec/models/project_invite_link_spec.rb View File

@@ -0,0 +1,5 @@
require 'rails_helper'

RSpec.describe ProjectInviteLink, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

Loading…
Cancel
Save