-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Implement project invitation system #3187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0d11472
f8c6ce6
96ce0d2
46d8a5b
7901be3
def1f7b
09218e4
aae59b6
54e1138
556958f
2804d73
33ff58a
f9cf577
18c69e9
eaf2218
546a8b7
41edfaa
fb40d67
2f30b61
71c3bfb
aea5384
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,9 +3,10 @@ package main | |
import ( | ||
"encoding/json" | ||
"fmt" | ||
"github.com/semaphoreui/semaphore/pkg/tz" | ||
"os" | ||
|
||
"github.com/semaphoreui/semaphore/pkg/tz" | ||
|
||
"github.com/go-gorp/gorp/v3" | ||
"github.com/semaphoreui/semaphore/db" | ||
"github.com/semaphoreui/semaphore/db/bolt" | ||
|
@@ -195,6 +196,35 @@ func addView() *db.View { | |
return &view | ||
} | ||
|
||
func addInvite() *db.ProjectInvite { | ||
invite, err := store.CreateProjectInvite(db.ProjectInvite{ | ||
ProjectID: userProject.ID, | ||
UserID: &userPathTestUser.ID, | ||
Email: &userPathTestUser.Email, | ||
Role: "owner", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Invite API Validation Fails on Dual User ID and EmailThe |
||
Status: db.ProjectInvitePending, | ||
Token: getUUID(), | ||
InviterUserID: testRunnerUser.ID, | ||
Created: tz.Now(), | ||
ExpiresAt: nil, // No expiration for this test | ||
AcceptedAt: nil, | ||
}) | ||
|
||
fmt.Println("***************************************") | ||
fmt.Println("***************************************") | ||
fmt.Println("***************************************") | ||
fmt.Println(invite.ID) | ||
fmt.Println("***************************************") | ||
fmt.Println("***************************************") | ||
fmt.Println("***************************************") | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
return &invite | ||
} | ||
|
||
func addSchedule() *db.Schedule { | ||
schedule, err := store.CreateSchedule(db.Schedule{ | ||
TemplateID: int(templateID), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -147,6 +147,87 @@ definitions: | |
type: string | ||
enum: [owner, manager, task_runner, guest] | ||
|
||
ProjectInvite: | ||
type: object | ||
properties: | ||
id: | ||
type: integer | ||
minimum: 1 | ||
project_id: | ||
type: integer | ||
minimum: 1 | ||
user_id: | ||
type: integer | ||
minimum: 1 | ||
description: User ID for user-based invites (optional) | ||
email: | ||
type: string | ||
format: email | ||
description: Email address for email-based invites (optional) | ||
role: | ||
type: string | ||
enum: [owner, manager, task_runner, guest] | ||
example: manager | ||
status: | ||
type: string | ||
enum: [pending, accepted, declined, expired] | ||
example: pending | ||
inviter_user_id: | ||
type: integer | ||
minimum: 1 | ||
description: ID of the user who created the invite | ||
created: | ||
type: string | ||
format: date-time | ||
description: When the invite was created | ||
expires_at: | ||
type: string | ||
format: date-time | ||
description: When the invite expires (optional) | ||
accepted_at: | ||
type: string | ||
format: date-time | ||
description: When the invite was accepted (optional) | ||
inviter_user: | ||
$ref: "#/definitions/User" | ||
description: Details of the user who created the invite | ||
user: | ||
$ref: "#/definitions/User" | ||
description: Details of the invited user (for user-based invites) | ||
|
||
ProjectInviteRequest: | ||
type: object | ||
properties: | ||
# user_id: | ||
# type: integer | ||
# minimum: 1 | ||
# description: User ID to invite (use either user_id or email, not both) | ||
email: | ||
type: string | ||
format: email | ||
description: Email address to invite (use either user_id or email, not both) | ||
x-example: [email protected] | ||
role: | ||
type: string | ||
enum: [owner, manager, task_runner, guest] | ||
example: manager | ||
expires_at: | ||
type: string | ||
format: date-time | ||
description: When the invite should expire (optional, defaults to 7 days) | ||
required: | ||
- role | ||
|
||
AcceptInviteRequest: | ||
type: object | ||
properties: | ||
token: | ||
type: string | ||
description: The invitation token | ||
x-example: "a1b2c3d4e5f6..." | ||
required: | ||
- token | ||
|
||
ProjectBackup: | ||
type: object | ||
example: {"meta":{"name":"homelab","alert":true,"alert_chat":"Test","max_parallel_tasks":0,"type":null},"templates":[{"inventory":"Build","repository":"Demo","environment":"Empty","name":"Build","playbook":"build.yml","arguments":"[]","allow_override_args_in_task":false,"description":"Build Job","vault_key":null,"type":"build","start_version":"1.0.0","build_template":null,"view":"Build","autorun":false,"survey_vars":[],"suppress_success_alerts":false,"cron":"* * * * *"}],"repositories":[{"name":"Demo","git_url":"https://github.com/semaphoreui/semaphore-demo.git","git_branch":"main","ssh_key":"None"}],"keys":[{"name":"None","type":"none"},{"name":"Vault Password","type":"login_password"}],"views":[{"title":"Build","position":0}],"inventories":[{"name":"Build","inventory":"","ssh_key":"None","become_key":"None","type":"static"},{"name":"Dev","inventory":"","ssh_key":"None","become_key":"None","type":"file"},{"name":"Prod","inventory":"","ssh_key":"None","become_key":"None","type":"file"}],"environments":[{"name":"Empty","password":null,"json":"{}","env":null}]} | ||
|
@@ -1150,6 +1231,13 @@ parameters: | |
type: integer | ||
required: true | ||
x-example: 13 | ||
invite_id: | ||
name: invite_id | ||
description: Invite ID | ||
in: path | ||
type: integer | ||
required: true | ||
x-example: 14 | ||
|
||
paths: | ||
/debug/gc: | ||
|
@@ -1654,6 +1742,120 @@ paths: | |
204: | ||
description: User updated | ||
|
||
# Invite management | ||
/project/{project_id}/invites: | ||
parameters: | ||
- $ref: "#/parameters/project_id" | ||
get: | ||
tags: | ||
- project | ||
summary: Get invitations for project | ||
parameters: | ||
- name: sort | ||
in: query | ||
required: false | ||
type: string | ||
enum: [created, status, role] | ||
description: sorting field | ||
x-example: created | ||
- name: order | ||
in: query | ||
required: false | ||
type: string | ||
enum: [asc, desc] | ||
description: ordering manner | ||
x-example: desc | ||
responses: | ||
200: | ||
description: Project invitations | ||
schema: | ||
type: array | ||
items: | ||
$ref: "#/definitions/ProjectInvite" | ||
post: | ||
tags: | ||
- project | ||
summary: Create project invitation | ||
parameters: | ||
- name: Invite | ||
in: body | ||
required: true | ||
schema: | ||
$ref: "#/definitions/ProjectInviteRequest" | ||
responses: | ||
201: | ||
description: Invitation created | ||
schema: | ||
$ref: "#/definitions/ProjectInvite" | ||
400: | ||
description: Bad request (invalid role, missing user_id/email, or both provided) | ||
409: | ||
description: User already a member or invitation already exists | ||
|
||
/project/{project_id}/invites/{invite_id}: | ||
parameters: | ||
- $ref: "#/parameters/project_id" | ||
- $ref: "#/parameters/invite_id" | ||
get: | ||
tags: | ||
- project | ||
summary: Get specific project invitation | ||
responses: | ||
200: | ||
description: Project invitation | ||
schema: | ||
$ref: "#/definitions/ProjectInvite" | ||
404: | ||
description: Invitation not found | ||
# put: | ||
# tags: | ||
# - project | ||
# summary: Update project invitation status | ||
# parameters: | ||
# - name: Invite Update | ||
# in: body | ||
# required: true | ||
# schema: | ||
# type: object | ||
# properties: | ||
# status: | ||
# type: string | ||
# enum: [pending, declined, expired] | ||
# example: declined | ||
# responses: | ||
# 204: | ||
# description: Invitation updated | ||
# 400: | ||
# description: Invalid status or status transition | ||
# delete: | ||
# tags: | ||
# - project | ||
# summary: Delete project invitation | ||
# responses: | ||
# 204: | ||
# description: Invitation deleted | ||
# | ||
# /invites/accept: | ||
# post: | ||
# tags: | ||
# - project | ||
# summary: Accept project invitation | ||
# parameters: | ||
# - name: Accept Invite | ||
# in: body | ||
# required: true | ||
# schema: | ||
# $ref: "#/definitions/AcceptInviteRequest" | ||
# responses: | ||
# 204: | ||
# description: Invitation accepted successfully | ||
# 400: | ||
# description: Invalid token, invitation expired, or user already a member | ||
# 403: | ||
# description: Invitation not for this user | ||
# 404: | ||
# description: Invitation not found | ||
|
||
/project/{project_id}/integrations: | ||
parameters: | ||
- $ref: "#/parameters/project_id" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Hardcoded Invite ID Causes Inconsistent Processing
The
invite_id
in thebodyFieldProcessor
function is hardcoded to4
. This is inconsistent with how other object IDs (e.g.,task.ID
,schedule.ID
,environmentID
,templateID
) are handled in the same function, which use dynamicinvite.ID
values. This inconsistency can lead to incorrectinvite_id
values being processed in requests.