-
Notifications
You must be signed in to change notification settings - Fork 0
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
Frontend Build Out #41
Conversation
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.
Main takeaways:
- Lots of progress, great work!
- Need to refactor UserID populating middleware (i.e. dump into a
session
package) - Seems like we should talk about API design and stuff
- No idea why invitations are the way they are
- No composable loops or calling outside of
setup
for general FE stability
(Approved, but maybe let's chat and do one more round of review before this goes in?)
cmd/server/main.go
Outdated
|
||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
ctx, err := fn(r.Context()) |
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.
nit: I think this will go away with the above comment, but just in case it doesn't: normally, one only returns a new context from functions that are dedicated to contexts, e.g. WithX
or context.WithX
functions. So I'd change it to something like:
fn := func(c context.Context) (pacta.UserID, error) { ... }
[...]
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uID, err := fn(r.Context())
ctx := session.WithUserID(ctx, uID)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
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.
Yep - Done.
} | ||
|
||
// Claims this initiative invitation, if it exists | ||
// (POST /initiative-invitation/{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.
nit: I think of "claiming an invitation" as a non-CRUD action that should get a custom verb, like POST /initiative-invitation/{id}:claim
. Benefits of this:
- Unambigious about what it's for, makes reading logs and stuff easier
- If you have another "updating an initiative invitation" endpoint you need to add, you won't have a collision on
POST /initiative-invitation/{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.
Done
return fmt.Errorf("looking up initiative invite: %w", err) | ||
} | ||
if !ii.UsedAt.IsZero() || ii.UsedBy != nil { | ||
if ii.UsedBy != nil && ii.UsedBy.ID == userID { |
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.
My usual grumbling about new paradigms I don't like: the whole "We use objects for everything and only populate the IDs" makes all of this logic one conditional comparision more than it otherwise would need to be. E.g. as compared to
if ii.UsedByID == userID { ... }
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.
Yeah I think in most cases you'll come to like this pattern. I've found it increasingly lovely for side projects.
// We may want to log this, though. | ||
return nil | ||
} else { | ||
return fmt.Errorf("initiative is already used: %+v", ii) |
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.
We should be using oapierr.BadRequest (or similar) for this. For this case in particular, we likely want a custom error code so we can show a specific error to the end user. Then you'd just need to update the error logic below to:
if errors.Is(err, oapierr.Error{}) { // or something like that, could also make oapierr.Is(err) or similar
return err
} else if err != nil {
return oapierr.Internal(...)
}
You could even make a lil helper, like return oapierr.OrInternal(err)
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.
Done, but through different means, as an external var.
const [ | ||
{ data: user }, | ||
] = await Promise.all([ | ||
useSimpleAsyncData(`${prefix}.findUserById`, () => pactaClient.findUserById(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.
Same note here, don't need Promise.all
if you only got one promise.
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.
Next PR adds more :)
console.info(message) | ||
// console.info(message) | ||
return | ||
case LogLevel.Verbose: | ||
console.debug(message) | ||
// console.debug(message) | ||
return | ||
case LogLevel.Warning: | ||
console.warn(message) | ||
return | ||
case LogLevel.Trace: | ||
console.trace(message) | ||
// console.trace(message) | ||
return |
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.
undo pls
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.
I've found this logging makes the console semi-unusable - sorting through hundreds of extraneous logs in order to find anything other than authn information. I think our default should be to a clean log. Keeping warning and error messages makes a ton of sense, but the default happy path of console logging should not create noise.
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.
^^
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.
Boosted a few previous comments that should be addressed/we should discuss, but otherwise LGTM
fn := func(c context.Context) (context.Context, error) { | ||
token, _, err := jwtauth.FromContext(c) | ||
if err != nil { | ||
return nil, fmt.Errorf("error getting authorization token: %w", err) | ||
} | ||
if token == nil { | ||
return nil, fmt.Errorf("nil authorization token") | ||
} | ||
authnID := token.Subject() | ||
if authnID == "" { | ||
return nil, fmt.Errorf("couldn't find authn id in jwt") | ||
} | ||
user, err := d.UserByAuthn(d.NoTxn(c), pacta.AuthnMechanism_EmailAndPass, authnID) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get user by authn: %w", err) | ||
} | ||
return WithUserID(c, user.ID), nil | ||
} | ||
|
||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
ctx, err := fn(r.Context()) | ||
if err != nil { | ||
// Optionally log errors here when debugging authentication access. | ||
// logger.Warn("couldn't authenticate", zap.Error(err)) | ||
// http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
next.ServeHTTP(w, r.WithContext(ctx)) | ||
}) | ||
} |
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.
A few random tweaks, just to abstract some concerns better. E.g. it'd be easier to pull loadUserID
into a function and test it independently now:
fn := func(c context.Context) (context.Context, error) { | |
token, _, err := jwtauth.FromContext(c) | |
if err != nil { | |
return nil, fmt.Errorf("error getting authorization token: %w", err) | |
} | |
if token == nil { | |
return nil, fmt.Errorf("nil authorization token") | |
} | |
authnID := token.Subject() | |
if authnID == "" { | |
return nil, fmt.Errorf("couldn't find authn id in jwt") | |
} | |
user, err := d.UserByAuthn(d.NoTxn(c), pacta.AuthnMechanism_EmailAndPass, authnID) | |
if err != nil { | |
return nil, fmt.Errorf("failed to get user by authn: %w", err) | |
} | |
return WithUserID(c, user.ID), nil | |
} | |
return func(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
ctx, err := fn(r.Context()) | |
if err != nil { | |
// Optionally log errors here when debugging authentication access. | |
// logger.Warn("couldn't authenticate", zap.Error(err)) | |
// http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | |
next.ServeHTTP(w, r) | |
return | |
} | |
next.ServeHTTP(w, r.WithContext(ctx)) | |
}) | |
} | |
loadUserID := func(ctx context.Context) (pacta.UserID, error) { | |
token, _, err := jwtauth.FromContext(ctx) | |
if err != nil { | |
return "", fmt.Errorf("error getting jwt from context: %w", err) | |
} | |
if token == nil { | |
return "", errors.New("nil authorization token") | |
} | |
authnID := token.Subject() | |
if authnID == "" { | |
return "", errors.New("no 'sub' claim in JWT") | |
} | |
user, err := d.UserByAuthn(d.NoTxn(ctx), pacta.AuthnMechanism_EmailAndPass, authnID) | |
if err != nil { | |
return "", fmt.Errorf("failed to load user %q by authn: %w", authnID, err) | |
} | |
return user.ID, nil | |
} | |
return func(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
ctx := r.Context() | |
userID, err := fn(ctx) | |
if err != nil { | |
// Optionally log errors here when debugging authentication access. | |
// logger.Warn("couldn't authenticate", zap.Error(err)) | |
// http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | |
next.ServeHTTP(w, r) | |
return | |
} | |
next.ServeHTTP(w, r.WithContext(WithUserID(ctx, userID))) | |
}) | |
} |
// Optionally log errors here when debugging authentication access. | ||
// logger.Warn("couldn't authenticate", zap.Error(err)) |
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.
Since we don't have any endpoints that are public, and since this runs after the JWT verification flow, if this fails, we want to fail the request here, as this will be a programmer bug 10 times out of 10. Otherwise, it's just going to go onto the handler, which will then fail because it couldn't load the UserID
. At least here, we catch it at the source. I'd log at ERROR
-level and throw a http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
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.
FYI We will have non-public endpoints: initiatives are going to be non-public. Does that change your reasoning?
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.
Discussed offline, there will be public endpoints. That said, our current middleware has no way to poke holes for public endpoints, so anything non-auth'd will fail at the moment
chimiddleware.Recoverer, | ||
|
||
jwtauth.Verifier(jwtauth.New("EdDSA", nil, jwKey)), | ||
jwtauth.Authenticator, | ||
session.WithAuthn(logger, db), |
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.
nothing needs to change here, but because auth in RMI-land is so different than AS-land, we really don't need a whole package and middleware for this, it could just be a server helper like s.getUser
or s.getUserID
. It's no more or less code in the endpoints themselves, just the difference between session.UserID(ctx)
and s.getUserID(ctx)
.
if i == nil { | ||
return nil, oapierr.Internal("initiativeInvitationToOAPI: can't convert nil pointer") | ||
} | ||
if !initiativeIDRegex.MatchString(i.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.
I can't remember if I asked this before, but why are we validating IDs, aren't we the ones generating them? And if we aren't the ones generating them, let's chat.
if i.UsedBy != nil { | ||
usedBy = ptr(string(i.UsedBy.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.
If you expect to be doing this a lot, you could make a generic nullablePtr
or similar helper, then inline this in the return
useSimpleAsyncData(`${prefix}.getInitiative`, () => pactaClient.findInitiativeById(id)), | ||
useSimpleAsyncData(`${prefix}.getInvitations`, () => pactaClient.listInitiativeInvitations(id)), | ||
useSimpleAsyncData(`${prefix}.getRelationships`, () => pactaClient.listInitiativeUserRelationshipsByInitiative(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.
^^
const [ | ||
{ data: initiative }, | ||
] = await Promise.all([ | ||
useSimpleAsyncData(`${prefix}.getInitiative`, () => pactaClient.findInitiativeById(id)), | ||
useSimpleAsyncData(`${prefix}.getInvitations`, () => pactaClient.listInitiativeInvitations(id)), | ||
useSimpleAsyncData(`${prefix}.getRelationships`, () => pactaClient.listInitiativeUserRelationshipsByInitiative(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.
^^
const [ | ||
{ data: relationships }, | ||
] = await Promise.all([ | ||
useSimpleAsyncData(`${prefix}.getRelationships`, () => pactaClient.listInitiativeUserRelationshipsByInitiative(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.
(this was resolved, but it's still a Promise.all
on a single promise)
console.info(message) | ||
// console.info(message) | ||
return | ||
case LogLevel.Verbose: | ||
console.debug(message) | ||
// console.debug(message) | ||
return | ||
case LogLevel.Warning: | ||
console.warn(message) | ||
return | ||
case LogLevel.Trace: | ||
console.trace(message) | ||
// console.trace(message) | ||
return |
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.
^^
@@ -0,0 +1,5 @@ | |||
export const useIsAuthenticated = () => { | |||
const prefix = 'useIsAuthenticated' | |||
const isAuthenticated = useState<boolean>(`${prefix}.isAuthenticated`, () => true) |
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.
Sorry, straggling comment: why does this default to true?
Discussed offline - hitting the timebox on this. Smaller, better PRs going forward. |
NOTE: This PR is a poorly structured amalgomation of a bunch of small changes. In the future I'll try to do better to structure changes like these as smaller, more focused PRs - just during this initial build out, so much is changing so quickly, I'm struggling to find the "right" boundaries for increments of work.
Contentious
main.go
method. I don't love this so if you have better Ideas on how this can be done, please let me know.useSession.signedIn
anduseMSAL.isAuthenticated
to fix a few issues with hydration mismtaches/SSR 500s.Non-Contentious