Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions ORGANIZATION_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# VPM Organization Support Feature

## Problem

Currently, VPM only allows users to publish packages from repositories under their own GitHub account. The validation in `check_vcs()` function requires the repository URL to start with `https://github.com/{username}/`, where `username` is the logged-in user's GitHub username.

This prevents users from publishing packages from GitHub organizations they belong to.

## Solution Overview

1. Add a new database table to store user's GitHub organization memberships
2. Fetch user's organizations during OAuth login
3. Modify `check_vcs()` to also accept organization URLs where the user is a member
4. Automatically use organization name as package prefix when publishing from org repos

## Files Changed

### New Files
- `src/entity/organization.v` - UserOrganization entity
- `src/repo/organization.v` - Database operations for organizations

### Modified Files
- `src/auth.v` - Fetch user's organizations during OAuth login
- `src/usecase/package/packages.v` - Support organization URLs and prefixes
- `src/package.v` - Pass organization info to create function

## How It Works

1. When a user logs in via GitHub OAuth, we fetch their organization memberships using the GitHub API (`/user/orgs`)
2. Organizations are stored in the `UserOrganization` table
3. When creating a package:
- The URL is validated against both the user's account AND their organizations
- If the URL belongs to an organization, the package name uses the org name as prefix (e.g., `v-hono.hono` instead of `meiseayoung.hono`)

## Example

User `meiseayoung` is a member of the `v-hono` organization.

Before this change:
- Publishing `https://github.com/v-hono/v-hono-core` would fail with "You must submit a package from your own account"

After this change:
- Publishing `https://github.com/v-hono/v-hono-core` with name `hono` creates package `v-hono.hono`

## Database Migration

Run the following to create the new table:

```sql
CREATE TABLE IF NOT EXISTS "UserOrganization" (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
org_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_user_org_user_id ON "UserOrganization" (user_id);
```

## GitHub OAuth Scope

Note: The GitHub OAuth app may need the `read:org` scope to access organization memberships. Update the OAuth authorization URL if needed:

```
https://github.com/login/oauth/authorize?response_type=code&client_id={CLIENT_ID}&scope=read:org
```
29 changes: 29 additions & 0 deletions src/auth.v
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import json
import vweb
import entity { User }
import lib.log
import repo

struct GitHubUser {
login string
}

struct GitHubOrg {
login string
}

const random = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890'

fn random_string(len int) string {
Expand Down Expand Up @@ -66,6 +71,30 @@ fn (mut app App) oauth_cb() vweb.Result {
random_id = app.db.q_string("select random_id from \"User\" where username='${login}' ") or {
panic(err)
}

// Fetch user's GitHub organizations
orgs_resp := http.fetch(
url: 'https://api.github.com/user/orgs'
method: .get
header: http.new_header(key: .authorization, value: 'token ${token}')
) or {
println('failed to fetch orgs: ${err}')
http.Response{}
}

if orgs_resp.status_code == 200 {
gh_orgs := json.decode([]GitHubOrg, orgs_resp.body) or { [] }
mut org_names := []string{cap: gh_orgs.len}
for org in gh_orgs {
org_names << org.login
}
// Save organizations to database
orgs_repo := repo.organizations(app.db)
orgs_repo.save_user_organizations(user_id, org_names) or {
println('failed to save orgs: ${err}')
}
}

app.set_cookie(
name: 'id'
value: user_id.str()
Expand Down
12 changes: 12 additions & 0 deletions src/entity/organization.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module entity

import time

@[json: 'user_organization']
pub struct UserOrganization {
pub mut:
id int @[primary; sql: serial]
user_id int
org_name string
created_at time.Time = time.now()
}
7 changes: 6 additions & 1 deletion src/package.v
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import lib.storage
import lib.html
import markdown
import entity { Package }
import repo

@['/new']
fn (mut app App) new() vweb.Result {
Expand All @@ -15,7 +16,11 @@ fn (mut app App) new() vweb.Result {

@['/create_package'; post]
pub fn (mut app App) create_package(name string, url string, description string) vweb.Result {
app.packages().create(name, url, description, app.cur_user) or {
// Get user's organizations
orgs_repo := repo.organizations(app.db)
user_orgs := orgs_repo.get_user_org_names(app.cur_user.id)

app.packages().create_with_orgs(name, url, description, app.cur_user, user_orgs) or {
log.error()
.add('error', err.str())
.add('url', url)
Expand Down
61 changes: 61 additions & 0 deletions src/repo/organization.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module repo

import orm
import entity { UserOrganization }

pub struct OrganizationsRepo {
mut:
db orm.Connection @[required]
}

pub fn migrate_organizations(db orm.Connection) ! {
sql db {
create table UserOrganization
}!
}

pub fn organizations(db orm.Connection) OrganizationsRepo {
return OrganizationsRepo{
db: db
}
}

pub fn (o OrganizationsRepo) get_user_organizations(user_id int) []UserOrganization {
return sql o.db {
select from UserOrganization where user_id == user_id
} or { [] }
}

pub fn (o OrganizationsRepo) get_user_org_names(user_id int) []string {
orgs := o.get_user_organizations(user_id)
mut names := []string{cap: orgs.len}
for org in orgs {
names << org.org_name
}
return names
}

pub fn (o OrganizationsRepo) user_belongs_to_org(user_id int, org_name string) bool {
orgs := sql o.db {
select from UserOrganization where user_id == user_id && org_name == org_name
} or { [] }
return orgs.len > 0
}

pub fn (o OrganizationsRepo) save_user_organizations(user_id int, org_names []string) ! {
// Delete existing organizations for this user
sql o.db {
delete from UserOrganization where user_id == user_id
} or {}

// Insert new organizations
for org_name in org_names {
org := UserOrganization{
user_id: user_id
org_name: org_name
}
sql o.db {
insert org into UserOrganization
} or { continue }
}
}
101 changes: 101 additions & 0 deletions src/repo/organization_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module repo

import entity { UserOrganization }

// Mock database for testing
struct MockOrmConnection {
mut:
orgs []UserOrganization
}

// Test UserOrganization entity
fn test_user_organization_creation() {
org := UserOrganization{
id: 1
user_id: 100
org_name: 'v-hono'
}

assert org.id == 1
assert org.user_id == 100
assert org.org_name == 'v-hono'
}

fn test_user_organization_multiple() {
orgs := [
UserOrganization{
id: 1
user_id: 100
org_name: 'v-hono'
},
UserOrganization{
id: 2
user_id: 100
org_name: 'vlang'
},
UserOrganization{
id: 3
user_id: 100
org_name: 'another-org'
},
]

assert orgs.len == 3
assert orgs[0].org_name == 'v-hono'
assert orgs[1].org_name == 'vlang'
assert orgs[2].org_name == 'another-org'
}

// Test helper function to extract org names
fn test_extract_org_names() {
orgs := [
UserOrganization{
id: 1
user_id: 100
org_name: 'v-hono'
},
UserOrganization{
id: 2
user_id: 100
org_name: 'vlang'
},
]

mut names := []string{cap: orgs.len}
for org in orgs {
names << org.org_name
}

assert names.len == 2
assert 'v-hono' in names
assert 'vlang' in names
}

// Test user belongs to org logic
fn check_membership(orgs []UserOrganization, org_name string) bool {
for org in orgs {
if org.org_name == org_name {
return true
}
}
return false
}

fn test_user_belongs_to_org_logic() {
orgs := [
UserOrganization{
id: 1
user_id: 100
org_name: 'v-hono'
},
UserOrganization{
id: 2
user_id: 100
org_name: 'vlang'
},
]

assert check_membership(orgs, 'v-hono') == true
assert check_membership(orgs, 'vlang') == true
assert check_membership(orgs, 'other-org') == false
}
1 change: 1 addition & 0 deletions src/repo/repo.v
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ pub fn migrate(db orm.Connection) ! {
migrate_categories(db)!
migrate_packages(db)!
migrate_users(db)!
migrate_organizations(db)!
}
Loading