diff --git a/tui/auth.go b/tui/auth.go index 15eb534..b649383 100644 --- a/tui/auth.go +++ b/tui/auth.go @@ -1,8 +1,44 @@ package main -import huh "github.com/charmbracelet/huh" +import ( + huh "github.com/charmbracelet/huh" +) type Auth struct { - form *huh.Form - jwt *string + loginForm *huh.Form + registerForm *huh.Form + jwt *string +} + +func getLoginForm() *huh.Form { + return huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Email"). + Key("email"), + huh.NewInput(). + Title("Password"). + Key("password"). + Password(true), + )).WithWidth(40) +} + +func getRegisterForm() *huh.Form { + return huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Email"). + Key("email"), + huh.NewInput(). + Title("Username"). + Key("username"), + huh.NewInput(). + Title("Password"). + Key("password"). + Password(true), + huh.NewInput(). + Title("Repeat Password"). + Key("password_repeat"). + Password(true), + )).WithWidth(40) } diff --git a/tui/entry.go b/tui/entry.go index b5adbb6..6af4fca 100644 --- a/tui/entry.go +++ b/tui/entry.go @@ -5,24 +5,24 @@ import ( ) type Feed struct { - id string - name string - url string - faviconUrl string - tags []string + Id string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + FaviconUrl string `json:"faviconUrl"` + Tags []string `json:"tags"` } type Entry struct { - id string - title string - content string - link string - date time.Time + Id string `json:"id"` + ArticleTitle string `json:"title"` + Content string `json:"content"` + Link string `json:"link"` + Date time.Time `json:"time"` - author *string // author not always specified - isRead bool - isBookmarked bool - isIgnored bool - isReadLater bool - feed Feed + Author *string `json:"author"` // author not always specified + IsRead bool `json:"isRead"` + IsBookmarked bool `json:"IsBookmarked"` + IsIgnored bool `json:"isIgnored"` + IsReadLater bool `json:"isReadLater"` + Feed Feed `json:"feed"` } diff --git a/tui/http.go b/tui/http.go index 6e75000..1dd4404 100644 --- a/tui/http.go +++ b/tui/http.go @@ -1,9 +1,11 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" + "log" "net/http" tea "github.com/charmbracelet/bubbletea" @@ -13,60 +15,167 @@ type statusMsg int type errMsg struct{ error } type missingJwtMsg struct{} +type noJwtMsg struct{} +type invalidJwtMsg struct{} +type httpErrorMsg error func (e errMsg) Error() string { return e.error.Error() } -type getEntriesSuccessMsg []Entry -type getEntriesErrMsg error - -const serverUrl = "localhost:3000" +const serverUrl = "http://localhost:1597" type loginSuccessMsg struct{ string } +type registerSuccessMsg struct{ string } + +func getData(req *http.Request) ([]byte, error) { + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Print("err", err) + return nil, httpErrorMsg(err) + } + defer resp.Body.Close() // nolint: errcheck + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, httpErrorMsg(err) + } + return data, nil + +} + +func checkJwt(jwt *string) tea.Cmd { + + return func() tea.Msg { + if jwt == nil || *jwt == "" { + return noJwtMsg{} + } + url := fmt.Sprintf("%s/me", serverUrl) + req, _ := http.NewRequest(http.MethodPost, url, nil) + req.Header.Add("Content-type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt)) + data, err := getData(req) + if err != nil { + return invalidJwtMsg{} + } + var resp struct { + Id string `json:"id"` + Name string `json:"name"` + } + err = json.Unmarshal(data, &resp) + if err != nil { + return invalidJwtMsg{} + } + return nil + } + +} func login(username string, password string) tea.Cmd { return func() tea.Msg { - _ = username - _ = password - return loginSuccessMsg{"dawdaw"} + url := fmt.Sprintf("%s/login", serverUrl) + body := struct { + Name string `json:"email"` + Password string `json:"password"` + }{ + Name: username, Password: password, + } + out, err := json.Marshal(body) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(out)) + req.Header.Add("Content-type", "application/json") + data, err := getData(req) + if err != nil { + return err + } + var loginResp AuthRes + + err = json.Unmarshal(data, &loginResp) + if err != nil { + return httpErrorMsg(err) + } + + return loginSuccessMsg{loginResp.Token} } } +type AuthRes struct { + Token string `json:"token"` +} + +func register(username string, password string, email string) tea.Cmd { + return func() tea.Msg { + + url := fmt.Sprintf("%s/register", serverUrl) + body := struct { + Name string `json:"name"` + Password string `json:"password"` + Email string `json:"email"` + }{ + Name: username, Password: password, Email: email, + } + out, err := json.Marshal(body) + if err != nil { + log.Fatal(err) + } + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(out)) + req.Header.Add("Content-type", "application/json") + data, err := getData(req) + if err != nil { + return err + } + var registerResp AuthRes + err = json.Unmarshal(data, ®isterResp) + if err != nil { + return httpErrorMsg(err) + } + return registerSuccessMsg{registerResp.Token} + } +} + +type getEntriesSuccessMsg []Entry + func getEntries(jwt *string) tea.Cmd { return func() tea.Msg { url := fmt.Sprintf("%s/entries", serverUrl) - - req, err := http.NewRequest(http.MethodGet, url, nil) + req, _ := http.NewRequest(http.MethodGet, url, nil) if jwt == nil { return missingJwtMsg{} } req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt)) - + req.Header.Add("Content-type", "application/json") + data, err := getData(req) if err != nil { - return getEntriesErrMsg(err) + return httpErrorMsg(err) } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return getEntriesErrMsg(err) - } - defer resp.Body.Close() // nolint: errcheck - - data, err := io.ReadAll(resp.Body) - if err != nil { - return getEntriesErrMsg(err) - } - var entries []Entry - err = json.Unmarshal(data, &entries) if err != nil { - return getEntriesErrMsg(err) + return httpErrorMsg(err) } - return getEntriesSuccessMsg(entries) } - } type getFeedsSuccessMsg []Feed -type getFeedsErrMsg error + +func getFeeds(jwt *string) tea.Cmd { + return func() tea.Msg { + url := fmt.Sprintf("%s/feeds", serverUrl) + req, _ := http.NewRequest(http.MethodGet, url, nil) + if jwt == nil { + return missingJwtMsg{} + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt)) + data, err := getData(req) + if err != nil { + return httpErrorMsg(err) + } + var feeds []Feed + err = json.Unmarshal(data, &feeds) + if err != nil { + return httpErrorMsg(err) + } + return getFeedsSuccessMsg(feeds) + } +} diff --git a/tui/main.go b/tui/main.go index b690ced..7f3b856 100644 --- a/tui/main.go +++ b/tui/main.go @@ -15,11 +15,11 @@ import ( ) func (e Entry) FilterValue() string { - return e.title + return e.ArticleTitle } func (e Entry) Title() string { - return e.title + return e.ArticleTitle } func (e Entry) Description() string { @@ -39,31 +39,12 @@ type Model struct { } func New() *Model { - loginForm := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Username"). - Key("username"), - // Validating fields is easy. The form will mark erroneous fields - // and display error messages accordingly. - // TODO: - // Validate(func(str string) error { - // if str == "octopus773" { - // return errors.New("Not you") - // } - // return nil - // }))) - huh.NewInput(). - Title("Password"). - Key("password"). - Password(true), - )) ti := textinput.New() ti.Placeholder = "Pikachu" ti.Focus() ti.CharLimit = 156 ti.Width = 56 - return &Model{textInput: ti, auth: Auth{form: loginForm, jwt: new(string)}, page: "LOGIN"} + return &Model{textInput: ti, auth: Auth{loginForm: getLoginForm(), registerForm: getRegisterForm(), jwt: new(string)}, page: LOGIN} } func (m Model) getEverything() tea.Cmd { @@ -76,16 +57,16 @@ func (m *Model) initList(width int, height int) { m.list = list.New([]list.Item{}, list.NewDefaultDelegate(), width, height) m.list.Title = "Posts" m.list.SetFilteringEnabled(false) - var f = Feed{id: "1", tags: []string{"Devops", "Kubernetes"}, name: "zwindler", url: "zwindler.blog", faviconUrl: "zwindler.blog.favicon"} + var f = Feed{Id: "1", Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"} m.list.SetItems([]list.Item{ - Entry{id: "1", title: "yay", content: "ouin ouin ouin", link: "awd", date: time.Now(), isRead: false, isIgnored: false, isReadLater: false, isBookmarked: false, feed: f}, - Entry{id: "2", title: "grrrrr", content: "ouin ouin ouin", link: "awd", date: time.Now(), isRead: false, isIgnored: false, isReadLater: false, isBookmarked: false, feed: f}, - Entry{id: "3", title: "my life is pain", content: "ouin ouin ouin", link: "awd", date: time.Now(), isRead: false, isIgnored: false, isReadLater: false, isBookmarked: false, feed: f}, + Entry{Id: "1", ArticleTitle: "yay", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, + Entry{Id: "2", ArticleTitle: "grrrrr", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, + Entry{Id: "3", ArticleTitle: "my life is pain", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, }) } func (m Model) Init() tea.Cmd { - return tea.Batch(checkServer, m.auth.form.Init()) + return tea.Batch(checkServer, m.auth.loginForm.Init(), m.auth.registerForm.Init(), checkJwt(m.auth.jwt)) } @@ -191,40 +172,78 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.initList(msg.Width, msg.Height) + + case invalidJwtMsg: + m.auth.jwt = new(string) + m.page = LOGIN + return m, nil case loginSuccessMsg: *m.auth.jwt = msg.string m.page = FEEDS + return m, m.getEverything() + + case registerSuccessMsg: + *m.auth.jwt = msg.string + m.page = FEEDS + return m, m.getEverything() case tea.KeyMsg: switch msg.Type { case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit + case tea.KeyCtrlT: + if m.page == LOGIN { + m.page = REGISTER + } else if m.page == REGISTER { + m.page = LOGIN + } + } switch { - case key.Matches(msg, m.textInput.KeyMap.DeleteCharacterBackward): + case key.Matches(msg, m.textInput.KeyMap.DeleteCharacterBackward): //TODO: add only when query words := strings.Split(m.textInput.Value(), " ") if len(words) > 0 && (strings.HasPrefix(words[len(words)-1], "tag:") || strings.HasPrefix(words[len(words)-1], "feed:")) { m.deleteWordBackward() } } - // if writing } var cmds []tea.Cmd // Process the form - form, cmd := m.auth.form.Update(msg) - if f, ok := form.(*huh.Form); ok { - m.auth.form = f - cmds = append(cmds, cmd) + // LOGIN + if m.page == LOGIN { + form, cmd := m.auth.loginForm.Update(msg) + if f, ok := form.(*huh.Form); ok { + m.auth.loginForm = f + cmds = append(cmds, cmd) + } + + if m.auth.loginForm.State == huh.StateCompleted { + username := m.auth.loginForm.GetString("email") + password := m.auth.loginForm.GetString("password") + cmds = append(cmds, login(username, password)) + } } - if m.auth.form.State == huh.StateCompleted { - // Quit when the form is done. - username := m.auth.form.GetString("username") - password := m.auth.form.GetString("password") - cmds = append(cmds, login(username, password)) - } + if m.page == REGISTER { + // Process the form + // LOGIN + registerForm, cmd := m.auth.registerForm.Update(msg) + if f, ok := registerForm.(*huh.Form); ok { + m.auth.registerForm = f + cmds = append(cmds, cmd) + } + + if m.auth.registerForm.State == huh.StateCompleted { + username := m.auth.registerForm.GetString("username") + password := m.auth.registerForm.GetString("password") + email := m.auth.registerForm.GetString("email") + cmds = append(cmds, register(username, password, email)) + } + + } + var cmd tea.Cmd m.list, cmd = m.list.Update(msg) cmds = append(cmds, cmd) m.textInput, cmd = m.textInput.Update(msg) @@ -240,7 +259,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func main() { - tea.LogToFile("yay.log", "") + tea.LogToFile("vex.log", "") m := New() p := tea.NewProgram(m) if _, err := p.Run(); err != nil { diff --git a/tui/pages.go b/tui/pages.go index ee62991..ea11a64 100644 --- a/tui/pages.go +++ b/tui/pages.go @@ -1,22 +1,33 @@ package main +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" +) + const ( - LOGIN = "LOGIN" - ENTRIES = "ENTRIES" - FEEDS = "FEEDS" - TAGS = "TAGS" + LOGIN = "LOGIN" + REGISTER = "REGISTER" + ENTRIES = "ENTRIES" + FEEDS = "FEEDS" + TAGS = "TAGS" ) type VexPage string func (m Model) LoginView() string { - return m.auth.form.View() + return lipgloss.JoinHorizontal( + lipgloss.Left, + m.auth.loginForm.View(), + m.auth.registerForm.View(), + ) } func (m Model) EntriesView() string { return "" } func (m Model) FeedsView() string { - return "feeds" + return fmt.Sprintf("%s ", *m.auth.jwt) } func (m Model) TagsView() string { return "" @@ -25,6 +36,8 @@ func (m Model) View() string { switch m.page { case LOGIN: return m.LoginView() + case REGISTER: + return m.LoginView() case ENTRIES: return m.EntriesView() case FEEDS: