Skip to content
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

API: POST /organization #32

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
9 changes: 9 additions & 0 deletions api/datastore/organization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package datastore

import (
"context"
)

func (d DataStore) CreateOrganization(ctx context.Context, descr, name, userID, website string) (string, error) {
return d.db.InsertOrganization(ctx, descr, name, userID, website)
}
1 change: 1 addition & 0 deletions api/datastore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
)

type DBClient interface {
InsertOrganization(ctx context.Context, descr, name, userID, website string) (string, error)
InsertUser(ctx context.Context, first, id, last, username string) error
QueryUserByID(context.Context, string) (User, error)
UpdateUser(ctx context.Context, first, id, last, username string) error
Expand Down
3 changes: 2 additions & 1 deletion api/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ func NewDB(ctx context.Context, connString string) (Client, error) {
return Client{}, fmt.Errorf("error parsing db url: %w", err)
}

// explicitly set this because fly.io create another default db
// explicitly set this because fly.io creates a different default db per-project, and I don't want
// to trust whatever the conn string in the env is set to.
u.Path = "/board"
cfg, err := pgxpool.ParseConfig(u.String())
if err != nil {
Expand Down
46 changes: 46 additions & 0 deletions api/db/organization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package db

import (
"context"
"fmt"
)

func (c *Client) InsertOrganization(ctx context.Context, descr, name, userID, website string) (string, error) {
query := `
WITH org_insert AS (
INSERT INTO organizations.organization AS o (created_by, owned_by)
SELECT user_id,
user_id
FROM users.user u
WHERE u.user_uuid = $1
RETURNING o.organization_id oid,
o.organization_uuid ouid,
u.user_id uid
),
prof_insert AS (
INSERT INTO organizations.profile (organization_id, name, description, website_url)
VALUES (org_insert.oid, $2, $3, $4)
),
rep_insert AS (
INSERT INTO organizations.representative AS rep (organization_id, user_id)
VALUES (org_insert.oid, org_insert.uid)
RETURNING rep.representative_id rid
)
INSERT INTO organizations.permissions (
organization_id,
representative_id,
update_profile,
create_job,
post_job
)
VALUES (org_insert.oid, rep_insert.rid, TRUE, TRUE, TRUE)
RETURNING org_insert.ouid org_id;
`

var oid string
if err := c.db.QueryRow(ctx, query, userID, name, descr, website).Scan(&oid); err != nil {
return "", fmt.Errorf("error inserting organization: %w", err)
}

return oid, nil
}
28 changes: 25 additions & 3 deletions api/server/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func (k *contextKey) String() string {
var userIDCTXKey = &contextKey{"userID"}

// ContextValue is a shortcut to fetch the value of type T from context.
func ContextValue[T any](ctx context.Context, key *contextKey) *T {
val, _ := ctx.Value(key).(*T)
func ContextValue[T any](ctx context.Context, key *contextKey) T {
val, _ := ctx.Value(key).(T)
return val
}

Expand Down Expand Up @@ -68,7 +68,7 @@ func (s *Server) withResourceOwner(param string) func(http.Handler) http.Handler
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := *ContextValue[string](ctx, userIDCTXKey)
id := ContextValue[string](ctx, userIDCTXKey)

p := chi.URLParam(r, param)
if p == "" {
Expand Down Expand Up @@ -107,3 +107,25 @@ func (s *Server) withResourceOwner(param string) func(http.Handler) http.Handler
})
}
}

func (s *Server) withVerifiedEmail(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := ContextValue[string](ctx, userIDCTXKey)

u, err := s.idStore.GetUser(id)
if err != nil {
hErr := HTTPError{http.StatusInternalServerError, "error getting user from id store"}
respond(w, r, nil, hErr, hErr.status)
return
}

if !u.Email.Verified {
hErr := HTTPError{http.StatusForbidden, "email not verified"}
respond(w, r, nil, hErr, hErr.status)
return
}

next.ServeHTTP(w, r)
})
}
53 changes: 53 additions & 0 deletions api/server/organization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package server

import (
"encoding/json"
"net/http"
)

type (
OrganizationResource struct {
Description string `json:"description"`
ID string `json:"id"`
Name string `json:"name"`
Website string `json:"website"`
}

PostOrganizationInput struct {
Description string `json:"description"`
ID string
Name string `json:"name"`
Website string `json:"website"`
}
)

func (s *Server) handlePostOrganization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := ContextValue[string](ctx, userIDCTXKey)

var in PostOrganizationInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
hErr := HTTPError{http.StatusUnprocessableEntity, "invalid JSON"}
respond(w, r, nil, hErr, 0)
return
}
in.ID = id

// TODO: bring in a validator. All other handlers should have one too.

oid, err := s.dataStore.CreateOrganization(ctx, in.Description, in.Name, id, in.Website)
if err != nil {
hErr := HTTPError{http.StatusInternalServerError, "error creating organization: " + err.Error()}
respond(w, r, nil, hErr, 0)
return
}

out := OrganizationResource{
Description: in.Description,
ID: oid,
Name: in.Name,
Website: in.Website,
}

respond(w, r, out, nil, http.StatusCreated)
}
7 changes: 7 additions & 0 deletions api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type AuthStore interface {
}

type DataStore interface {
CreateOrganization(ctx context.Context, descr, name, userID, website string) (string, error)
CreateUser(context.Context, PostUserInput) error
DeleteUser(context.Context, DeleteUserInput) error
GetUser(context.Context, GetUserInput) (UserMetadata, error)
Expand Down Expand Up @@ -95,6 +96,12 @@ func (s *Server) routes() {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})

r.Route("/organization", func(r chi.Router) {
r.With(withUserSession(true)).
With(s.withVerifiedEmail).
Post("/", s.handlePostOrganization)
})

r.Route("/user", func(r chi.Router) {
r.Get("/", s.handleGetUserList)
r.Post("/", s.handlePostUser)
Expand Down
6 changes: 3 additions & 3 deletions api/server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ var UserNotFound = HTTPError{http.StatusNotFound, "User not found"}

func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := *ContextValue[string](ctx, userIDCTXKey)
id := ContextValue[string](ctx, userIDCTXKey)

if err := s.dataStore.DeleteUser(ctx, DeleteUserInput{id}); err != nil {
respond(w, r, nil, err, 0)
Expand All @@ -72,7 +72,7 @@ func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {

func (s *Server) handleGetCurrentUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := *ContextValue[string](ctx, userIDCTXKey)
id := ContextValue[string](ctx, userIDCTXKey)

ui, err := s.idStore.GetUser(id)
if err != nil {
Expand Down Expand Up @@ -138,7 +138,7 @@ func (s *Server) handlePostUser(w http.ResponseWriter, r *http.Request) {

func (s *Server) handlePutUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := *ContextValue[string](ctx, userIDCTXKey)
id := ContextValue[string](ctx, userIDCTXKey)

var in PutUserInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
meta {
name: Health
name: Health check endpoint
type: http
seq: 1
}

get {
url: /health
url: http://localhost:8080/health
body: none
auth: none
}
11 changes: 11 additions & 0 deletions bruno/Sac Tech Job Board API/Job/getJobByID.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: getJobByID
type: http
seq: 2
}

get {
url: http://localhost:8080/job/{job_id}
body: none
auth: none
}
16 changes: 16 additions & 0 deletions bruno/Sac Tech Job Board API/Job/getJobPosting.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
meta {
name: getJobPosting
type: http
seq: 4
}

get {
url: http://localhost:8080/job/{job_id}/posting
body: none
auth: none
}

query {
Page Size:
~Page Token:
}
11 changes: 11 additions & 0 deletions bruno/Sac Tech Job Board API/Job/getJobPostingList.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: getJobPostingList
type: http
seq: 7
}

get {
url: http://localhost:8080/job/posting
body: none
auth: none
}
18 changes: 18 additions & 0 deletions bruno/Sac Tech Job Board API/Job/postJob.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
meta {
name: postJob
type: http
seq: 1
}

post {
url: http://localhost:8080/job
body: json
auth: none
}

body:json {
{
"title": "",
"description": ""
}
}
22 changes: 22 additions & 0 deletions bruno/Sac Tech Job Board API/Job/postJobPosting.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
meta {
name: postJobPosting
type: http
seq: 5
}

post {
url: http://localhost:8080/job/{job_id}/posting
body: json
auth: none
}

body:json {
{
"employmentType": "",
"payRate": "",
"payType": "",
"startDate": "",
"endDate": "",
"publishOverride": ""
}
}
11 changes: 11 additions & 0 deletions bruno/Sac Tech Job Board API/Job/putJobByID.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: putJobByID
type: http
seq: 3
}

put {
url: http://localhost:8080/job/{job_id}
body: json
auth: none
}
11 changes: 11 additions & 0 deletions bruno/Sac Tech Job Board API/Job/putJobPostingByID.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: putJobPostingByID
type: http
seq: 6
}

put {
url: http://localhost:8080/job/{job_id}/posting
body: json
auth: none
}
11 changes: 11 additions & 0 deletions bruno/Sac Tech Job Board API/Organization/getOrganizationByID.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: getOrganizationByID
type: http
seq: 2
}

get {
url: http://localhost:8080/organization/{org_id}
body: none
auth: none
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: getOrganizationJobList
type: http
seq: 4
}

get {
url: http://localhost:8080/organization/jobs
body: none
auth: none
}
19 changes: 19 additions & 0 deletions bruno/Sac Tech Job Board API/Organization/postOrganization.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
meta {
name: postOrganization
type: http
seq: 1
}

post {
url: http://localhost:8080/organization
body: json
auth: none
}

body:json {
{
"name": "",
"description": "",
"website": ""
}
}
11 changes: 11 additions & 0 deletions bruno/Sac Tech Job Board API/Organization/putOrganizationByID.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: putOrganizationByID
type: http
seq: 3
}

put {
url: http://localhost:8080/organization/{org_id}
body: json
auth: none
}
Loading