diff --git a/server/cmd/main.go b/server/cmd/main.go index 22d9753..dedd5b9 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -161,8 +161,9 @@ func handler(opts options, f http.HandlerFunc) http.HandlerFunc { // transport encryption and then, on the server side, proper salted hashing. type User struct { - // TODO - ID string + ID string + Username string + Hash string } type BBJResponse struct { @@ -177,39 +178,55 @@ func writeResponse(w http.ResponseWriter, resp BBJResponse) { json.NewEncoder(w).Encode(resp) } +// NB breaking: i'm not just returning 200 always but using http status codes func writeErrorResponse(w http.ResponseWriter, code int, resp BBJResponse) { w.WriteHeader(code) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } -// NB breaking: i'm not just returning 200 always but using http status codes - -type AuthInfo struct { - Username string - Hash string -} - -func getAuthInfo(opts options, req *http.Request) (ai AuthInfo, err error) { - ai.Username = req.Header.Get("User") - ai.Hash = req.Header.Get("Auth") - if ai.Username == "" { +func getUserFromReq(opts options, req *http.Request) (u User, err error) { + u.Username = req.Header.Get("User") + u.Hash = req.Header.Get("Auth") + if u.Username == "" { err = errors.New("no User header set") - } - - if ai.Username == "anon" { - if !opts.Config.AllowAnon { - err = errors.New("anonymous access disabled") - } return } - err = checkAuth(opts, ai) + if u.Username == "anon" { + if !opts.Config.AllowAnon { + err = errors.New("anonymous access disabled") + return + } + } + + db := opts.DB + stmt, err := db.Prepare("select auth_hash, id from users where user_name = ?") + if err != nil { + err = fmt.Errorf("db error: %w", err) + return + } + defer stmt.Close() + + opts.Logf("querying for %s", u.Username) + + var authHash string + if err = stmt.QueryRow(u.Username).Scan(&authHash, u.ID); err != nil { + if strings.Contains(err.Error(), "no rows in result") { + err = errors.New("no such user") + } else { + err = fmt.Errorf("db error: %w", err) + } + } + + if authHash != u.Hash { + err = errors.New("bad credentials") + } return } -func checkAuth(opts options, ai AuthInfo) error { +func checkAuth(opts options, username, hash string) error { db := opts.DB stmt, err := db.Prepare("select auth_hash from users where user_name = ?") if err != nil { @@ -217,17 +234,17 @@ func checkAuth(opts options, ai AuthInfo) error { } defer stmt.Close() - opts.Logf("querying for %s", ai.Username) + opts.Logf("querying for %s", username) var authHash string - if err = stmt.QueryRow(ai.Username).Scan(&authHash); err != nil { + if err = stmt.QueryRow(username).Scan(&authHash); err != nil { if strings.Contains(err.Error(), "no rows in result") { return errors.New("no such user") } return fmt.Errorf("db error: %w", err) } - if authHash != ai.Hash { + if authHash != hash { return errors.New("bad credentials") } @@ -296,7 +313,7 @@ func setupAPI(opts options) { opts.Logf("querying for %s", args.Username) - if err := checkAuth(opts, AuthInfo{args.Username, args.Hash}); err == nil { + if err := checkAuth(opts, args.Username, args.Hash); err == nil { opts.Logf("found %s", args.Username) // code 4 apparently writeErrorResponse(w, 403, BBJResponse{ @@ -415,7 +432,7 @@ func setupAPI(opts options) { } // TODO make this getUserInfoFromReq or similar so we can use the user ID later - authInfo, err := getAuthInfo(opts, req) + user, err := getUserFromReq(opts, req) if err != nil { writeErrorResponse(w, 403, BBJResponse{ Error: true, @@ -455,8 +472,6 @@ func setupAPI(opts options) { } defer stmt.Close() - // TODO user id, not username - threadID, err := uuid.NewRandom() if err != nil { serverErr(w, err) @@ -465,11 +480,11 @@ func setupAPI(opts options) { now := time.Now() if _, err = stmt.Exec( threadID, - authInfo.Username, + user.ID, args.Title, now, now, - authInfo.Username, + user.Username, ); err != nil { serverErr(w, err) return @@ -484,7 +499,7 @@ func setupAPI(opts options) { if _, err = stmt.Exec( threadID, - authInfo.Username, + user.ID, now, args.Body, args.SendRaw, @@ -498,8 +513,83 @@ func setupAPI(opts options) { return } - // TODO return the thread + stmt, err = db.Prepare("select * from threads where thread_id = ? limit 1") + if err != nil { + serverErr(w, err) + return + } + defer stmt.Close() - writeResponse(w, BBJResponse{Data: "TODO"}) + t := &Thread{} + + // TODO fill in rest of thread + if err = stmt.QueryRow(threadID).Scan( + t.ID, + t.Author, + t.Title, + t.LastMod, + t.Created, + t.ReplyCount, + t.Pinned, + t.LastAuthor, + ); err != nil { + serverErr(w, err) + return + } + + stmt, err = db.Prepare("select * from messages where thread_id = ?") + if err != nil { + serverErr(w, err) + return + } + defer stmt.Close() + rows, err := stmt.Query(threadID) + if err != nil { + serverErr(w, err) + return + } + + t.Messages = []Message{} + + for rows.Next() { + m := &Message{} + if err := rows.Scan( + m.ThreadID, + m.PostID, + m.Author, + m.Created, + m.Edited, + m.Body, + m.SendRaw, + ); err != nil { + serverErr(w, err) + return + } + t.Messages = append(t.Messages, *m) + } + + writeResponse(w, BBJResponse{Data: t}) })) } + +type Thread struct { + ID string `json:"thread_id"` + Author string + Title string + LastMod time.Time `json:"last_mod"` + Created time.Time + ReplyCount int `json:"reply_count"` + Pinned int // TODO bool + LastAuthor string `json:"last_author"` + Messages []Message +} + +type Message struct { + ThreadID string `json:"thread_id"` + PostID string `json:"post_id"` + Author string + Created time.Time + Edited int // TODO bool + Body string + SendRaw int `json:"send_raw"` // TODO bool +} diff --git a/server/cmd/schema.sql b/server/cmd/schema.sql index 52cfb0f..eb72af6 100644 --- a/server/cmd/schema.sql +++ b/server/cmd/schema.sql @@ -27,6 +27,7 @@ insert into users values ( ); -- TODO unique constraint on user_name? +-- TODO foreign keys create table threads ( thread_id text, -- uuid string