Skip to content

Commit 09a2315

Browse files
authored
Frontend Build Out (#41)
1 parent 675275b commit 09a2315

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2441
-118
lines changed

cmd/server/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ go_library(
1818
"//oapierr",
1919
"//openapi:pacta_generated",
2020
"//secrets",
21+
"//session",
2122
"//task",
2223
"@com_github_azure_azure_sdk_for_go_sdk_azcore//:azcore",
2324
"@com_github_azure_azure_sdk_for_go_sdk_azidentity//:azidentity",

cmd/server/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/RMI/pacta/oapierr"
2525
oapipacta "github.com/RMI/pacta/openapi/pacta"
2626
"github.com/RMI/pacta/secrets"
27+
"github.com/RMI/pacta/session"
2728
"github.com/RMI/pacta/task"
2829
"github.com/Silicon-Ally/cryptorand"
2930
"github.com/Silicon-Ally/zaphttplog"
@@ -312,11 +313,12 @@ func run(args []string) error {
312313
// LogEntry created by the logging middleware.
313314
chimiddleware.RequestID,
314315
chimiddleware.RealIP,
315-
zaphttplog.NewMiddleware(logger),
316+
zaphttplog.NewMiddleware(logger, zaphttplog.WithConcise(true)),
316317
chimiddleware.Recoverer,
317318

318319
jwtauth.Verifier(jwtauth.New("EdDSA", nil, jwKey)),
319320
jwtauth.Authenticator,
321+
session.WithAuthn(logger, db),
320322

321323
oapimiddleware.OapiRequestValidator(pactaSwagger),
322324

cmd/server/pactasrv/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ go_library(
44
name = "pactasrv",
55
srcs = [
66
"initiative.go",
7+
"initiative_invitation.go",
8+
"initiative_user_relationship.go",
79
"pacta_version.go",
810
"pactasrv.go",
911
"portfolio.go",
@@ -18,6 +20,7 @@ go_library(
1820
"//oapierr",
1921
"//openapi:pacta_generated",
2022
"//pacta",
23+
"//session",
2124
"//task",
2225
"@com_github_go_chi_jwtauth_v5//:jwtauth",
2326
"@com_github_google_uuid//:uuid",

cmd/server/pactasrv/conv/oapi_to_pacta.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ func PactaVersionCreateFromOAPI(p *api.PactaVersionCreate) (*pacta.PACTAVersion,
5151
}, nil
5252
}
5353

54+
func InitiativeInvitationFromOAPI(i *api.InitiativeInvitationCreate) (*pacta.InitiativeInvitation, error) {
55+
if i == nil {
56+
return nil, oapierr.Internal("initiativeInvitationToOAPI: can't convert nil pointer")
57+
}
58+
if !initiativeIDRegex.MatchString(i.Id) {
59+
return nil, oapierr.BadRequest("id must contain only alphanumeric characters, underscores, and dashes")
60+
}
61+
if i.InitiativeId == "" {
62+
return nil, oapierr.BadRequest("initiative_id must not be empty")
63+
}
64+
return &pacta.InitiativeInvitation{
65+
ID: pacta.InitiativeInvitationID(i.Id),
66+
Initiative: &pacta.Initiative{ID: pacta.InitiativeID(i.InitiativeId)},
67+
}, nil
68+
}
69+
5470
func ifNil[T any](t *T, fallback T) T {
5571
if t == nil {
5672
return fallback

cmd/server/pactasrv/conv/pacta_to_oapi.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,46 @@ func PactaVersionToOAPI(pv *pacta.PACTAVersion) (*api.PactaVersion, error) {
5454
}, nil
5555
}
5656

57+
func InitiativeInvitationToOAPI(i *pacta.InitiativeInvitation) (*api.InitiativeInvitation, error) {
58+
if i == nil {
59+
return nil, oapierr.Internal("initiativeToOAPI: can't convert nil pointer")
60+
}
61+
var usedAt *string
62+
if !i.UsedAt.IsZero() {
63+
usedAt = ptr(i.UsedAt.String())
64+
}
65+
var usedBy *string
66+
if i.UsedBy != nil {
67+
usedBy = ptr(string(i.UsedBy.ID))
68+
}
69+
return &api.InitiativeInvitation{
70+
CreatedAt: i.CreatedAt,
71+
Id: string(i.ID),
72+
InitiativeId: string(i.Initiative.ID),
73+
UsedAt: usedAt,
74+
UsedByUserId: usedBy,
75+
}, nil
76+
}
77+
78+
func InitiativeUserRelationshipToOAPI(i *pacta.InitiativeUserRelationship) (*api.InitiativeUserRelationship, error) {
79+
if i == nil {
80+
return nil, oapierr.Internal("initiativeUserRelationshipToOAPI: can't convert nil pointer")
81+
}
82+
if i.User == nil {
83+
return nil, oapierr.Internal("initiativeUserRelationshipToOAPI: can't convert nil user")
84+
}
85+
if i.Initiative == nil {
86+
return nil, oapierr.Internal("initiativeUserRelationshipToOAPI: can't convert nil initiative")
87+
}
88+
return &api.InitiativeUserRelationship{
89+
UpdatedAt: i.UpdatedAt,
90+
InitiativeId: string(i.Initiative.ID),
91+
UserId: string(i.User.ID),
92+
Manager: i.Manager,
93+
Member: i.Member,
94+
}, nil
95+
}
96+
5797
func ptr[T any](t T) *T {
5898
return &t
5999
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package pactasrv
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/RMI/pacta/cmd/server/pactasrv/conv"
9+
"github.com/RMI/pacta/db"
10+
"github.com/RMI/pacta/oapierr"
11+
api "github.com/RMI/pacta/openapi/pacta"
12+
"github.com/RMI/pacta/pacta"
13+
"go.uber.org/zap"
14+
)
15+
16+
// Creates an initiative invitation
17+
// (POST /initiative-invitation)
18+
func (s *Server) CreateInitiativeInvitation(ctx context.Context, request api.CreateInitiativeInvitationRequestObject) (api.CreateInitiativeInvitationResponseObject, error) {
19+
// TODO(#12) Implement Authorization
20+
ii, err := conv.InitiativeInvitationFromOAPI(request.Body)
21+
if err != nil {
22+
return nil, err
23+
}
24+
id, err := s.DB.CreateInitiativeInvitation(s.DB.NoTxn(ctx), ii)
25+
if err != nil {
26+
return nil, oapierr.Internal("failed to create initiative invitation", zap.Error(err))
27+
}
28+
if id != ii.ID {
29+
return nil, oapierr.Internal(
30+
"failed to create initiative invitation: ID mismatch",
31+
zap.String("requested_id", string(ii.ID)),
32+
zap.String("actual_id", string(id)),
33+
)
34+
}
35+
return api.CreateInitiativeInvitation204Response{}, nil
36+
}
37+
38+
// Deletes an initiative invitation by id
39+
// (DELETE /initiative-invitation/{id})
40+
func (s *Server) DeleteInitiativeInvitation(ctx context.Context, request api.DeleteInitiativeInvitationRequestObject) (api.DeleteInitiativeInvitationResponseObject, error) {
41+
// TODO(#12) Implement Authorization
42+
err := s.DB.DeleteInitiativeInvitation(s.DB.NoTxn(ctx), pacta.InitiativeInvitationID(request.Id))
43+
if err != nil {
44+
return nil, oapierr.Internal("failed to delete initiative invitation", zap.Error(err))
45+
}
46+
return api.DeleteInitiativeInvitation204Response{}, nil
47+
}
48+
49+
// Returns the initiative invitation from this id, if it exists
50+
// (GET /initiative-invitation/{id})
51+
func (s *Server) GetInitiativeInvitation(ctx context.Context, request api.GetInitiativeInvitationRequestObject) (api.GetInitiativeInvitationResponseObject, error) {
52+
// TODO(#12) Implement Authorization
53+
ii, err := s.DB.InitiativeInvitation(s.DB.NoTxn(ctx), pacta.InitiativeInvitationID(request.Id))
54+
if err != nil {
55+
return nil, oapierr.Internal("failed to retrieve initiative invitation", zap.Error(err))
56+
}
57+
result, err := conv.InitiativeInvitationToOAPI(ii)
58+
if err != nil {
59+
return nil, err
60+
}
61+
return api.GetInitiativeInvitation200JSONResponse(*result), nil
62+
}
63+
64+
// Claims this initiative invitation, if it exists
65+
// (POST /initiative-invitation/{id}:claim)
66+
func (s *Server) ClaimInitiativeInvitation(ctx context.Context, request api.ClaimInitiativeInvitationRequestObject) (api.ClaimInitiativeInvitationResponseObject, error) {
67+
userID, err := getUserID(ctx)
68+
if err != nil {
69+
return nil, err
70+
}
71+
var customErr api.ClaimInitiativeInvitationResponseObject
72+
err = s.DB.Transactional(ctx, func(tx db.Tx) error {
73+
ii, err := s.DB.InitiativeInvitation(tx, pacta.InitiativeInvitationID(request.Id))
74+
if err != nil {
75+
return fmt.Errorf("looking up initiative invite: %w", err)
76+
}
77+
if !ii.UsedAt.IsZero() || ii.UsedBy != nil {
78+
if ii.UsedBy != nil && ii.UsedBy.ID == userID {
79+
// We don't return an error if the same user tries to claim the same invitation twice,
80+
// which might happen by accident, but wouldn't impact the state of the initiative memberships.
81+
// We may want to log this, though.
82+
return nil
83+
} else {
84+
customErr = api.ClaimInitiativeInvitation409Response{}
85+
return fmt.Errorf("initiative is already used: %+v", ii)
86+
}
87+
}
88+
err = s.DB.UpdateInitiativeInvitation(tx, ii.ID,
89+
db.SetInitiativeInvitationUsedAt(time.Now()),
90+
db.SetInitiativeInvitationUsedBy(userID))
91+
if err != nil {
92+
return fmt.Errorf("updating initiative invite: %w", err)
93+
}
94+
err = s.DB.UpdateInitiativeUserRelationship(tx, ii.Initiative.ID, userID,
95+
db.SetInitiativeUserRelationshipMember(true))
96+
if err != nil {
97+
return fmt.Errorf("creating initiative membership: %w", err)
98+
}
99+
return nil
100+
})
101+
if err != nil {
102+
if customErr != nil {
103+
return customErr, nil
104+
}
105+
return nil, oapierr.Internal("failed to claim initiative invitation", zap.Error(err))
106+
}
107+
return api.ClaimInitiativeInvitation204Response{}, nil
108+
}
109+
110+
// Returns all initiative invitations associated with the initiative
111+
// (GET /initiative/{id}/invitations)
112+
func (s *Server) ListInitiativeInvitations(ctx context.Context, request api.ListInitiativeInvitationsRequestObject) (api.ListInitiativeInvitationsResponseObject, error) {
113+
// TODO(#12) Implement Authorization
114+
iis, err := s.DB.InitiativeInvitationsByInitiative(s.DB.NoTxn(ctx), pacta.InitiativeID(request.Id))
115+
if err != nil {
116+
return nil, oapierr.Internal("failed to list initiative invitations", zap.Error(err))
117+
}
118+
result, err := dereference(mapAll(iis, conv.InitiativeInvitationToOAPI))
119+
if err != nil {
120+
return nil, err
121+
}
122+
return api.ListInitiativeInvitations200JSONResponse(result), nil
123+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package pactasrv
2+
3+
import (
4+
"context"
5+
6+
"github.com/RMI/pacta/cmd/server/pactasrv/conv"
7+
"github.com/RMI/pacta/db"
8+
"github.com/RMI/pacta/oapierr"
9+
api "github.com/RMI/pacta/openapi/pacta"
10+
"github.com/RMI/pacta/pacta"
11+
"go.uber.org/zap"
12+
)
13+
14+
// Returns all initiative user relationships for the user that the user has access to view
15+
// (GET /initiative/{id}/user-relationships)
16+
func (s *Server) ListInitiativeUserRelationshipsByUser(ctx context.Context, request api.ListInitiativeUserRelationshipsByUserRequestObject) (api.ListInitiativeUserRelationshipsByUserResponseObject, error) {
17+
// TODO(#12) Implement Authorization
18+
iurs, err := s.DB.InitiativeUserRelationshipsByUser(s.DB.NoTxn(ctx), pacta.UserID(request.UserId))
19+
if err != nil {
20+
return nil, oapierr.Internal("failed to retrieve initiative user relationships by user", zap.Error(err))
21+
}
22+
result, err := dereference(mapAll(iurs, conv.InitiativeUserRelationshipToOAPI))
23+
if err != nil {
24+
return nil, err
25+
}
26+
return api.ListInitiativeUserRelationshipsByUser200JSONResponse(result), nil
27+
}
28+
29+
// Returns all initiative user relationships for the initiative that the user has access to view
30+
// (GET /initiative/user-relationships/{id})
31+
func (s *Server) ListInitiativeUserRelationshipsByInitiative(ctx context.Context, request api.ListInitiativeUserRelationshipsByInitiativeRequestObject) (api.ListInitiativeUserRelationshipsByInitiativeResponseObject, error) {
32+
// TODO(#12) Implement Authorization
33+
iurs, err := s.DB.InitiativeUserRelationshipsByInitiatives(s.DB.NoTxn(ctx), pacta.InitiativeID(request.InitiativeId))
34+
if err != nil {
35+
return nil, oapierr.Internal("failed to retrieve initiative user relationships by user", zap.Error(err))
36+
}
37+
result, err := dereference(mapAll(iurs, conv.InitiativeUserRelationshipToOAPI))
38+
if err != nil {
39+
return nil, err
40+
}
41+
return api.ListInitiativeUserRelationshipsByInitiative200JSONResponse(result), nil
42+
}
43+
44+
// Returns the initiative user relationship from this id, if it exists
45+
// (GET /initiative/{initiativeId}/user-relationship/{userId})
46+
func (s *Server) GetInitiativeUserRelationship(ctx context.Context, request api.GetInitiativeUserRelationshipRequestObject) (api.GetInitiativeUserRelationshipResponseObject, error) {
47+
// TODO(#12) Implement Authorization
48+
iur, err := s.DB.InitiativeUserRelationship(s.DB.NoTxn(ctx), pacta.InitiativeID(request.InitiativeId), pacta.UserID(request.UserId))
49+
if err != nil {
50+
return nil, oapierr.Internal("failed to retrieve initiative user relationship", zap.Error(err))
51+
}
52+
result, err := conv.InitiativeUserRelationshipToOAPI(iur)
53+
if err != nil {
54+
return nil, err
55+
}
56+
return api.GetInitiativeUserRelationship200JSONResponse(*result), nil
57+
}
58+
59+
// Updates initiative user relationship properties
60+
// (PATCH /initiative/{initiativeId}/user-relationship/{userId})
61+
func (s *Server) UpdateInitiativeUserRelationship(ctx context.Context, request api.UpdateInitiativeUserRelationshipRequestObject) (api.UpdateInitiativeUserRelationshipResponseObject, error) {
62+
// TODO(#12) Implement Authorization
63+
mutations := []db.UpdateInitiativeUserRelationshipFn{}
64+
if request.Body.Manager != nil {
65+
mutations = append(mutations, db.SetInitiativeUserRelationshipManager(*request.Body.Manager))
66+
}
67+
if request.Body.Member != nil {
68+
mutations = append(mutations, db.SetInitiativeUserRelationshipMember(*request.Body.Member))
69+
}
70+
err := s.DB.UpdateInitiativeUserRelationship(s.DB.NoTxn(ctx), pacta.InitiativeID(request.InitiativeId), pacta.UserID(request.UserId), mutations...)
71+
if err != nil {
72+
return nil, oapierr.Internal("failed to update initiative user relationship", zap.Error(err))
73+
}
74+
return api.UpdateInitiativeUserRelationship204Response{}, nil
75+
}

cmd/server/pactasrv/pactasrv.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/RMI/pacta/db"
99
"github.com/RMI/pacta/oapierr"
1010
"github.com/RMI/pacta/pacta"
11+
"github.com/RMI/pacta/session"
1112
"github.com/RMI/pacta/task"
1213
"go.uber.org/zap"
1314
)
@@ -44,6 +45,7 @@ type DB interface {
4445
InitiativeUserRelationshipsByUser(tx db.Tx, uid pacta.UserID) ([]*pacta.InitiativeUserRelationship, error)
4546
InitiativeUserRelationshipsByInitiatives(tx db.Tx, iid pacta.InitiativeID) ([]*pacta.InitiativeUserRelationship, error)
4647
PutInitiativeUserRelationship(tx db.Tx, iur *pacta.InitiativeUserRelationship) error
48+
UpdateInitiativeUserRelationship(tx db.Tx, iid pacta.InitiativeID, uid pacta.UserID, mutations ...db.UpdateInitiativeUserRelationshipFn) error
4749

4850
Initiative(tx db.Tx, id pacta.InitiativeID) (*pacta.Initiative, error)
4951
Initiatives(tx db.Tx, ids []pacta.InitiativeID) (map[pacta.InitiativeID]*pacta.Initiative, error)
@@ -65,7 +67,7 @@ type DB interface {
6567
CreatePortfolioInitiativeMembership(tx db.Tx, pim *pacta.PortfolioInitiativeMembership) error
6668
DeletePortfolioInitiativeMembership(tx db.Tx, pid pacta.PortfolioID, iid pacta.InitiativeID) error
6769

68-
GetOrCreateUserByAuthn(tx db.Tx, authnMechanism pacta.AuthnMechanism, authnID, enteredEmail, canonicalEmail string) (*pacta.User, error)
70+
GetOrCreateUserByAuthn(tx db.Tx, mech pacta.AuthnMechanism, authnID, email, canonicalEmail string) (*pacta.User, error)
6971
User(tx db.Tx, id pacta.UserID) (*pacta.User, error)
7072
Users(tx db.Tx, ids []pacta.UserID) (map[pacta.UserID]*pacta.User, error)
7173
UpdateUser(tx db.Tx, id pacta.UserID, mutations ...db.UpdateUserFn) error
@@ -82,10 +84,9 @@ type Blob interface {
8284
}
8385

8486
type Server struct {
85-
DB DB
86-
TaskRunner TaskRunner
87-
Logger *zap.Logger
88-
87+
DB DB
88+
TaskRunner TaskRunner
89+
Logger *zap.Logger
8990
Blob Blob
9091
PorfolioUploadURI string
9192
}
@@ -115,3 +116,11 @@ func dereference[T any](ts []*T, e error) ([]T, error) {
115116
}
116117
return result, nil
117118
}
119+
120+
func getUserID(ctx context.Context) (pacta.UserID, error) {
121+
userID, err := session.UserIDFromContext(ctx)
122+
if err != nil {
123+
return "", oapierr.Unauthorized("error getting authorization token", zap.Error(err))
124+
}
125+
return pacta.UserID(userID), nil
126+
}

0 commit comments

Comments
 (0)