Skip to content
Merged
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
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ coverage:
status:
project:
default:
target: 75%
target: 70%
informational: yes

ignore:
Expand Down
46 changes: 36 additions & 10 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func New(options *Options) http.Handler {
users := &users{
Database: options.Database,
}
workspaces := &workspaces{
Database: options.Database,
}

r := chi.NewRouter()
r.Route("/api/v2", func(r chi.Router) {
Expand All @@ -36,14 +39,15 @@ func New(options *Options) http.Handler {
})
r.Post("/login", users.loginWithPassword)
r.Post("/logout", users.logout)
// Used for setup.
r.Post("/user", users.createInitialUser)
r.Route("/users", func(r chi.Router) {
r.Post("/", users.createInitialUser)

r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
)
r.Post("/", users.createUser)
r.Group(func(r chi.Router) {
r.Use(
httpmw.ExtractAPIKey(options.Database, nil),
httpmw.ExtractUserParam(options.Database),
)
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{user}", users.user)
r.Get("/{user}/organizations", users.userOrganizations)
})
Expand All @@ -58,11 +62,33 @@ func New(options *Options) http.Handler {
r.Get("/", projects.allProjectsForOrganization)
r.Post("/", projects.createProject)
r.Route("/{project}", func(r chi.Router) {
r.Use(httpmw.ExtractProjectParameter(options.Database))
r.Use(httpmw.ExtractProjectParam(options.Database))
r.Get("/", projects.project)
r.Route("/versions", func(r chi.Router) {
r.Get("/", projects.projectVersions)
r.Post("/", projects.createProjectVersion)
r.Route("/history", func(r chi.Router) {
r.Get("/", projects.allProjectHistory)
r.Post("/", projects.createProjectHistory)
})
r.Get("/workspaces", workspaces.allWorkspacesForProject)
})
})
})

// Listing operations specific to resources should go under
// their respective routes. eg. /orgs/<name>/workspaces
r.Route("/workspaces", func(r chi.Router) {
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
r.Get("/", workspaces.listAllWorkspaces)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", workspaces.listAllWorkspaces)
r.Post("/", workspaces.createWorkspaceForUser)
r.Route("/{workspace}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
r.Get("/", workspaces.singleWorkspace)
r.Route("/history", func(r chi.Router) {
r.Post("/", workspaces.createWorkspaceHistory)
r.Get("/", workspaces.listAllWorkspaceHistory)
r.Get("/latest", workspaces.latestWorkspaceHistory)
})
})
})
Expand Down
34 changes: 19 additions & 15 deletions coderd/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
// abstracted for ease of change later on.
type Project database.Project

// ProjectVersion is the JSON representation of a Coder project version.
type ProjectVersion struct {
// ProjectHistory is the JSON representation of Coder project version history.
type ProjectHistory struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
CreatedAt time.Time `json:"created_at"`
Expand All @@ -42,7 +42,6 @@ type CreateProjectRequest struct {

// CreateProjectVersionRequest enables callers to create a new Project Version.
type CreateProjectVersionRequest struct {
Name string `json:"name,omitempty" validate:"username"`
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
}
Expand All @@ -51,7 +50,7 @@ type projects struct {
Database database.Store
}

// allProjects lists all projects across organizations for a user.
// Lists all projects the authenticated user has access to.
func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
organizations, err := p.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID)
Expand Down Expand Up @@ -79,7 +78,7 @@ func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, projects)
}

// allProjectsForOrganization lists all projects for a specific organization.
// Lists all projects in an organization.
func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
projects, err := p.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID})
Expand All @@ -96,7 +95,7 @@ func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Re
render.JSON(rw, r, projects)
}

// createProject makes a new project in an organization.
// Creates a new project in an organization.
func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
var createProject CreateProjectRequest
if !httpapi.Read(rw, r, &createProject) {
Expand Down Expand Up @@ -142,16 +141,16 @@ func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, project)
}

// project returns a single project parsed from the URL path.
// Returns a single project.
func (*projects) project(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)

render.Status(r, http.StatusOK)
render.JSON(rw, r, project)
}

// projectVersions lists versions for a single project.
func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
// Lists history for a single project.
func (p *projects) allProjectHistory(rw http.ResponseWriter, r *http.Request) {
project := httpmw.ProjectParam(r)

history, err := p.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
Expand All @@ -164,15 +163,18 @@ func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
})
return
}
versions := make([]ProjectVersion, 0)
apiHistory := make([]ProjectHistory, 0)
for _, version := range history {
versions = append(versions, convertProjectHistory(version))
apiHistory = append(apiHistory, convertProjectHistory(version))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, versions)
render.JSON(rw, r, apiHistory)
}

func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request) {
// Creates a new version of the project. An import job is queued to parse
// the storage method provided. Once completed, the import job will specify
// the version as latest.
func (p *projects) createProjectHistory(rw http.ResponseWriter, r *http.Request) {
var createProjectVersion CreateProjectVersionRequest
if !httpapi.Read(rw, r, &createProjectVersion) {
return
Expand Down Expand Up @@ -204,6 +206,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
Name: namesgenerator.GetRandomName(1),
StorageMethod: createProjectVersion.StorageMethod,
StorageSource: createProjectVersion.StorageSource,
// TODO: Make this do something!
ImportJobID: uuid.New(),
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand All @@ -218,8 +222,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
render.JSON(rw, r, convertProjectHistory(history))
}

func convertProjectHistory(history database.ProjectHistory) ProjectVersion {
return ProjectVersion{
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
return ProjectHistory{
ID: history.ID,
ProjectID: history.ProjectID,
CreatedAt: history.CreatedAt,
Expand Down
13 changes: 5 additions & 8 deletions coderd/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestProjects(t *testing.T) {
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 0)
})
Expand All @@ -127,13 +127,12 @@ func TestProjects(t *testing.T) {
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<10))
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "moo",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
require.NoError(t, err)
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
require.NoError(t, err)
require.Len(t, versions, 1)
})
Expand All @@ -156,8 +155,7 @@ func TestProjects(t *testing.T) {
require.NoError(t, err)
_, err = writer.Write(make([]byte, 1<<21))
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "moo",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: buffer.Bytes(),
})
Expand All @@ -173,8 +171,7 @@ func TestProjects(t *testing.T) {
Provisioner: database.ProvisionerTypeTerraform,
})
require.NoError(t, err)
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
Name: "moo",
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
StorageMethod: database.ProjectStorageMethodInlineArchive,
StorageSource: []byte{},
})
Expand Down
82 changes: 73 additions & 9 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,31 @@ import (
"github.com/coder/coder/httpmw"
)

// User is the JSON representation of a Coder user.
// User represents a user in Coder.
type User struct {
ID string `json:"id" validate:"required"`
Email string `json:"email" validate:"required"`
CreatedAt time.Time `json:"created_at" validate:"required"`
Username string `json:"username" validate:"required"`
}

// CreateInitialUserRequest enables callers to create a new user.
// CreateInitialUserRequest provides options to create the initial
// user for a Coder deployment. The organization provided will be
// created as well.
type CreateInitialUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
Password string `json:"password" validate:"required"`
Organization string `json:"organization" validate:"required,username"`
}

// CreateUserRequest provides options for creating a new user.
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
Password string `json:"password" validate:"required"`
}

// LoginWithPasswordRequest enables callers to authenticate with email and password.
type LoginWithPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
Expand Down Expand Up @@ -123,20 +132,66 @@ func (users *users) createInitialUser(rw http.ResponseWriter, r *http.Request) {
}

render.Status(r, http.StatusCreated)
render.JSON(rw, r, user)
render.JSON(rw, r, convertUser(user))
}

// Creates a new user.
func (users *users) createUser(rw http.ResponseWriter, r *http.Request) {
var createUser CreateUserRequest
if !httpapi.Read(rw, r, &createUser) {
return
}
_, err := users.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Username: createUser.Username,
Email: createUser.Email,
})
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "user already exists",
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get user: %s", err),
})
return
}

hashedPassword, err := userpassword.Hash(createUser.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("hash password: %s", err.Error()),
})
return
}

user, err := users.Database.InsertUser(r.Context(), database.InsertUserParams{
ID: uuid.NewString(),
Email: createUser.Email,
HashedPassword: []byte(hashedPassword),
Username: createUser.Username,
LoginType: database.LoginTypeBuiltIn,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("create user: %s", err.Error()),
})
return
}

render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertUser(user))
}

// Returns the parameterized user requested. All validation
// is completed in the middleware for this route.
func (*users) user(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)

render.JSON(rw, r, User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
})
render.JSON(rw, r, convertUser(user))
}

// Returns organizations the parameterized user has access to.
Expand Down Expand Up @@ -265,3 +320,12 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
}
return id, secret, nil
}

func convertUser(user database.User) User {
return User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
}
}
24 changes: 24 additions & 0 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,30 @@ func TestUsers(t *testing.T) {
require.NoError(t, err)
require.Len(t, orgs, 1)
})

t.Run("CreateUser", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
_ = server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: "tomato",
Password: "bananas",
})
require.NoError(t, err)
})

t.Run("CreateUserConflict", func(t *testing.T) {
t.Parallel()
server := coderdtest.New(t)
user := server.RandomInitialUser(t)
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
Email: "wow@ok.io",
Username: user.Username,
Password: "bananas",
})
require.Error(t, err)
})
}

func TestLogout(t *testing.T) {
Expand Down
Loading