-
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
Develop - PR 1 #1
base: main
Are you sure you want to change the base?
Conversation
crypto.go
Outdated
// hash are hex-encoded strings. | ||
func GenHash(password, salt string) string { | ||
passwordIn := []byte(password) | ||
saltIn, _ := hex.DecodeString(salt) |
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.
Don't drop errors here (and everywhere else) return the error to the caller and let the caller make the decision.
return &Env{db}, nil | ||
} | ||
|
||
func main() { |
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 don't think you want to put everything in your main package (main.go
and database.go
). Your main
package (and file) should basically just parse flags and call some library to do the actual work. This will also make it easier for you to write tests because you can target just that functionality.
For example you may want to do something like below.
/pkg/server/server.go
/pkg/server/server_test.go
/pkg/database/database.go
/pkg/database/database_test.go
/cmd
/cmd/server.go
Then /cmd/server.go
which will be your main package would look something like the following:
package main
import (
"example.com/pkg/server"
)
func main() {
var hostport := flag.String("hostport", ":8080", "help message for flag")
server, err := server.New(&server.Config{
HostPort: *hostport,
})
if err != nil {
log.Fatal(err)
}
if err := server.Start(); err != nil {
log.Fatal(err)
}
}
You don't have to use this exact structure, but having everything in the main package isn't a good idea.
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.
https://github.com/golang-standards/project-layout
I didn't realize that there was a standard(ish) Go project structure. Thanks for introducing it to me!
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 reviewed only the front-end part, for the initial PR overall looks good except the error handling part, I left a few comments to address. Once addressed, consider the frontend part approved.
frontend/src/Login.tsx
Outdated
} else { | ||
this.setState({ errorMessage: 'Invalid email/password.' }); | ||
} |
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.
lets add a proper error handling, right now all API errors get swallowed by the api.tsx
so I suggest to propagate an actual error and display it to the user.
frontend/src/api.tsx
Outdated
return response.status === 200; | ||
} catch (error) { | ||
return false; | ||
} |
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.
either return a promise as is and let the caller to handle it, or you can so something like:
return { result, error }
so it could be used like so:
const { result, error } = api.login(xxxx)
const { error } = api.login(xxxx)
if (error) {
///
}
|
||
func (env *Env) login(c *gin.Context) { | ||
now := time.Now() | ||
csrfToken := c.Request.Header.Get("CSRF") |
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.
Check that it's not empty?
internal/server/server.go
Outdated
func (env *Env) logout(c *gin.Context) { | ||
// TODO: move logged in check to its own function | ||
now := time.Now() | ||
csrfToken := c.Request.Header.Get("CSRF") |
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.
Check that it's not empty?
internal/server/server.go
Outdated
} | ||
|
||
func (env *Env) login(c *gin.Context) { | ||
now := time.Now() |
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: Move this closer to where it's used.
internal/server/server.go
Outdated
|
||
func (env *Env) logout(c *gin.Context) { | ||
// TODO: move logged in check to its own function | ||
now := time.Now() |
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: Move this closer to where it's used.
internal/server/server.go
Outdated
|
||
func Run(env *Env) error { | ||
go func() { | ||
tick := time.NewTicker(time.Minute) |
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.
defer tick.Close()
|
||
err = env.db.DeleteSession(sessionToken) | ||
if err != nil { | ||
log.Printf("Error deleting session: %v\n", 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.
Should this return unsuccessful status here if we failed to delete the session?
internal/server/server.go
Outdated
} | ||
|
||
func (env *Env) logout(c *gin.Context) { | ||
// TODO: move logged in check to its own function |
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 if you move this logic for checking csrf token, retrieving and checking the sesion etc, that would make it a bit cleaner.
internal/database/database.go
Outdated
DeleteExpiredSessions(t time.Time) error | ||
|
||
// Close closes any underlying resources. | ||
Close() |
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.
Curious why doesn't this return an error? If it did, it'd make it compatible with io.Closer
.
internal/database/database.go
Outdated
} | ||
|
||
func (db *MySqlDatabase) Close() { | ||
db.driver.Close() |
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.
Can this return an error?
internal/database/database.go
Outdated
} | ||
|
||
func (db *MySqlDatabase) DeleteExpiredSessions(t time.Time) error { | ||
stmt := "DELETE FROM Sessions WHERE expire_idle < ? OR expire_abs < ?" |
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.
Wouldn't this always delete any session that's existed for longer than 30 minutes (current expire_idle duration) - even if it's still "active"? I couldn't see expire_idle field being updated anywhere.
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.
expire_idle would be updated when a user makes an authenticated request. For the level 1 task, however, the only authenticated request is logout.
internal/server/server.go
Outdated
go func() { | ||
tick := time.NewTicker(time.Minute) | ||
for { | ||
now := <-tick.C | ||
log.Println("Clearing expired sessions") | ||
err := env.db.DeleteExpiredSessions(now) | ||
if err != nil { | ||
log.Printf("Error while cleaning up expired sessions: %v\n", 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.
This logic should be internal to your database and not exposed to the caller. So start the goroutine in NewMySqlDatabase
.
internal/database/database.go
Outdated
return &MySqlDatabase{driver}, nil | ||
} | ||
|
||
func (db *MySqlDatabase) CreatePreAuthSession(csrfToken string, initTime time.Time) (*types.Session, error) { |
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.
Instead of passing in time.Now()
to each function,. the common Go pattern is to create a fake clock, something like the following:
import (
"github.com/jonboulle/clockwork"
)
type MySqlDatabase struct {
driver *sql.DB
clock clockwork.Clock
}
func NewMySqlDatabase(username, password, databaseName string, clock clockwork.Clock) (*MySqlDatabase, error) {
[...]
if clock == nil {
clock = clockwork.NewRealClock()
}
return &MySqlDatabase{
[...]
clock: clock,
}
}
Now the caller does not have to worry about time and you can also fake time in tests.
internal/types/types.go
Outdated
// Expired checks if the session has expired. | ||
func (session *Session) Expired(t time.Time) bool { | ||
return t.After(session.ExpireIdle) || t.After(session.ExpireAbs) | ||
} |
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.
Three things:
- Do you need this function, or can you update your
FetchSession
query to not fetch expired sessions then let your cleanup loop remove it whenever it's expired. - The way you are setting
ExpireIdle
andExpireAbs
I think this will always trigger onExpireIdle
because you are not doing any renewal as far as I can tell. - Renewal is not in scope, so just set a maximum session life time for your session. Upon logout just remove it. No need for idle timeouts.
internal/server/server.go
Outdated
return | ||
} | ||
|
||
if session.Expired(now) { |
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 update your query to not return expired sessions, you don't need this function.
internal/server/server.go
Outdated
isCorrectPassword, err := crypto.IsCorrectPassword(account, password) | ||
if err != nil { | ||
log.Println("Salt stored in database is incorrectly encoded") | ||
c.AbortWithStatus(http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
if !isCorrectPassword { | ||
log.Println("Bad password") | ||
c.AbortWithStatus(http.StatusUnauthorized) | ||
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.
I don't think you need to split this out, just consolidate both checks and into crypto.IsCorrectPassword
and return an error if either one fails.
This part of the implementation should be feature complete and just needs feedback.