From 87aab8b23ba1cada047a525c78118e1970af906b Mon Sep 17 00:00:00 2001 From: overflowerror Date: Sat, 14 Aug 2021 22:29:24 +0200 Subject: [PATCH] feat(accounts): login with twitter works now I'm f-ing tired. --- backend/internal/app/data.go | 9 ++- backend/internal/app/logic.go | 7 +- backend/internal/data/account.go | 33 +++++++- backend/internal/data/models/account.go | 8 +- backend/internal/data/user.go | 2 +- backend/internal/logic/account.go | 36 ++++++++- backend/internal/logic/auth.go | 7 -- backend/internal/logic/error.go | 10 +++ backend/internal/logic/setup.go | 2 +- backend/internal/logic/twitter.go | 82 +++++++++++++++++++- backend/internal/presentation/account.go | 45 ++++++++++- backend/internal/presentation/dto/account.go | 10 +++ backend/internal/presentation/responses.go | 7 ++ backend/internal/router/routes.go | 2 + 14 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 backend/internal/logic/error.go create mode 100644 backend/internal/presentation/dto/account.go diff --git a/backend/internal/app/data.go b/backend/internal/app/data.go index e4a1c52..0f08bc6 100644 --- a/backend/internal/app/data.go +++ b/backend/internal/app/data.go @@ -4,7 +4,7 @@ import "threadule/backend/internal/data/models" type Data interface { CountUsers() (int64, error) - CreateUser(user *models.User) error + AddUser(user *models.User) error GetUserByUsername(username string) (*models.User, error) AddUserToGroup(user *models.User, group *models.Group) error DeleteUserFromGroup(user *models.User, group *models.Group) error @@ -16,11 +16,14 @@ type Data interface { UpdateSession(session *models.Session) error CleanupSessions() error - UpdateTweet(tweet *models.Tweet) error - GetAccountsByUser(user *models.User) ([]models.Account, error) + GetAccountById(user *models.User, id string) (*models.Account, error) + AddAccount(account *models.Account) error + UpdateAccount(account *models.Account) error GetScheduledThreads() ([]models.Thread, error) GetTweetsForThread(thread *models.Thread) ([]models.Tweet, error) UpdateThread(thread *models.Thread) error + + UpdateTweet(tweet *models.Tweet) error } diff --git a/backend/internal/app/logic.go b/backend/internal/app/logic.go index 672f087..025135d 100644 --- a/backend/internal/app/logic.go +++ b/backend/internal/app/logic.go @@ -1,10 +1,15 @@ package app -import "threadule/backend/internal/data/models" +import ( + "net/url" + "threadule/backend/internal/data/models" +) type Logic interface { AuthenticateSession(token string) (*models.User, error) Login(username, password string) (string, error) GetAccounts(user *models.User) ([]models.Account, error) + AddAccount(user *models.User) (string, *url.URL, error) + AddAccountResolve(user *models.User, id string, pin string) (*models.Account, error) } diff --git a/backend/internal/data/account.go b/backend/internal/data/account.go index db80458..ad92fab 100644 --- a/backend/internal/data/account.go +++ b/backend/internal/data/account.go @@ -1,6 +1,9 @@ package data -import "threadule/backend/internal/data/models" +import ( + "gorm.io/gorm/clause" + "threadule/backend/internal/data/models" +) func (d *Data) GetAccountsByUser(user *models.User) ([]models.Account, error) { var accounts []models.Account @@ -19,3 +22,31 @@ func (d *Data) GetAccountsByUser(user *models.User) ([]models.Account, error) { return accounts, nil } } + +func (d *Data) GetAccountById(user *models.User, id string) (*models.Account, error) { + var account models.Account + err := d.db. + Where("user_id = ?", user.ID). + Where("id = ?", id). + First(&account). + Error + if err != nil { + return nil, err + } else { + return &account, nil + } +} + +func (d *Data) AddAccount(account *models.Account) error { + return d.db. + Omit(clause.Associations). + Create(account). + Error +} + +func (d *Data) UpdateAccount(account *models.Account) error { + return d.db. + Omit(clause.Associations). + Save(account). + Error +} diff --git a/backend/internal/data/models/account.go b/backend/internal/data/models/account.go index 84e56e7..5604a33 100644 --- a/backend/internal/data/models/account.go +++ b/backend/internal/data/models/account.go @@ -7,7 +7,13 @@ type Account struct { UserID uuid.UUID User *User - Name string + ScreenName string + TwitterHandle string + TwitterID *int64 + AvatarURL string + + RequestToken *string + RequestSecret *string AccessToken *string AccessTokenSecret *string } diff --git a/backend/internal/data/user.go b/backend/internal/data/user.go index 91cc431..c9ee0ab 100644 --- a/backend/internal/data/user.go +++ b/backend/internal/data/user.go @@ -14,7 +14,7 @@ func (d *Data) CountUsers() (int64, error) { return c, err } -func (d *Data) CreateUser(user *models.User) error { +func (d *Data) AddUser(user *models.User) error { return d.db. Omit(clause.Associations). Create(user). diff --git a/backend/internal/logic/account.go b/backend/internal/logic/account.go index 5d4c11e..045582b 100644 --- a/backend/internal/logic/account.go +++ b/backend/internal/logic/account.go @@ -1,7 +1,41 @@ package logic -import "threadule/backend/internal/data/models" +import ( + "net/url" + "threadule/backend/internal/data/models" +) func (l *Logic) GetAccounts(user *models.User) ([]models.Account, error) { return l.ctx.Data.GetAccountsByUser(user) } + +func (l *Logic) AddAccount(user *models.User) (string, *url.URL, error) { + var account models.Account + account.AccessToken = nil + account.AccessTokenSecret = nil + account.RequestSecret = nil + account.UserID = user.ID + + err := l.ctx.Data.AddAccount(&account) + if err != nil { + l.ctx.Log.Errorf("couldn't create account in database: %v", err) + return "", nil, ErrInternalError + } + + return l.twitterLoginInit(&account) +} + +func (l *Logic) AddAccountResolve(user *models.User, id string, pin string) (*models.Account, error) { + account, err := l.ctx.Data.GetAccountById(user, id) + if err != nil { + l.ctx.Log.Errorf("couldn't get account for id: %v", err) + return nil, ErrInvalidParameter + } + + err = l.twitterLoginResolve(account, pin) + if err != nil { + return nil, err + } + + return account, nil +} diff --git a/backend/internal/logic/auth.go b/backend/internal/logic/auth.go index 2cb54fe..e508ce0 100644 --- a/backend/internal/logic/auth.go +++ b/backend/internal/logic/auth.go @@ -1,19 +1,12 @@ package logic import ( - "errors" "threadule/backend/internal/data/models" "time" ) const sessionDuration = 7 * 24 * time.Hour -var ( - ErrLoginFailed = errors.New("login failed") - ErrInvalidSession = errors.New("invalid session") - ErrInternalError = errors.New("something went wrong") -) - func (l *Logic) scheduleTriggerAuth() { err := l.ctx.Data.CleanupSessions() if err != nil { diff --git a/backend/internal/logic/error.go b/backend/internal/logic/error.go new file mode 100644 index 0000000..99e1c0a --- /dev/null +++ b/backend/internal/logic/error.go @@ -0,0 +1,10 @@ +package logic + +import "errors" + +var ( + ErrLoginFailed = errors.New("login failed") + ErrInvalidSession = errors.New("invalid session") + ErrInternalError = errors.New("something went wrong") + ErrInvalidParameter = errors.New("invalid parameter") +) diff --git a/backend/internal/logic/setup.go b/backend/internal/logic/setup.go index a2f580c..9f8d0a8 100644 --- a/backend/internal/logic/setup.go +++ b/backend/internal/logic/setup.go @@ -49,7 +49,7 @@ func (l *Logic) firstTimeSetup() error { // if this fails we can't recover anyway l.ctx.Log.Fatal(err) } - err = l.ctx.Data.CreateUser(adminUser) + err = l.ctx.Data.AddUser(adminUser) if err != nil { l.ctx.Log.Errorf("couldn't create admin user: %v", err) return err diff --git a/backend/internal/logic/twitter.go b/backend/internal/logic/twitter.go index 521fe49..07bd7a0 100644 --- a/backend/internal/logic/twitter.go +++ b/backend/internal/logic/twitter.go @@ -1,10 +1,12 @@ package logic import ( + "net/url" "threadule/backend/internal/data/models" ) import "github.com/dghubble/oauth1" import "github.com/dghubble/go-twitter/twitter" +import twitterOAuth "github.com/dghubble/oauth1/twitter" func (l *Logic) sendTweet(client *twitter.Client, tweet *models.Tweet, prevId int64) (int64, error) { status, _, err := client.Statuses.Update( @@ -35,7 +37,7 @@ func (l *Logic) sendTweet(client *twitter.Client, tweet *models.Tweet, prevId in return status.ID, nil } -func (l *Logic) getTwitterClient(account *models.Account) *twitter.Client { +func (l *Logic) getAcountClient(account *models.Account) *twitter.Client { config := oauth1.NewConfig( l.ctx.Config.Twitter.ConsumerKey, l.ctx.Config.Twitter.ConsumerSecret, @@ -50,7 +52,7 @@ func (l *Logic) getTwitterClient(account *models.Account) *twitter.Client { } func (l *Logic) sendThread(thread *models.Thread) { - client := l.getTwitterClient(thread.Account) + client := l.getAcountClient(thread.Account) thread.Status = models.ThreadProcessing err := l.ctx.Data.UpdateThread(thread) @@ -120,3 +122,79 @@ func (l *Logic) scheduleTriggerTwitter() { l.sendThread(&thread) } } + +func (l *Logic) getTwitterOAuthConfig() *oauth1.Config { + return &oauth1.Config{ + ConsumerKey: l.ctx.Config.Twitter.ConsumerKey, + ConsumerSecret: l.ctx.Config.Twitter.ConsumerSecret, + CallbackURL: "oob", + Endpoint: twitterOAuth.AuthorizeEndpoint, + } +} + +func (l *Logic) twitterLoginInit(account *models.Account) (string, *url.URL, error) { + oauth1Config := l.getTwitterOAuthConfig() + + requestToken, requestSecret, err := oauth1Config.RequestToken() + if err != nil { + l.ctx.Log.Errorf("couldn't get requestToken: %v", err) + return "", nil, ErrInternalError + } + + account.RequestToken = &requestToken + account.RequestSecret = &requestSecret + err = l.ctx.Data.UpdateAccount(account) + if err != nil { + l.ctx.Log.Errorf("couldn't update account in database: %v", err) + return "", nil, ErrInternalError + } + + authUrl, err := oauth1Config.AuthorizationURL(requestToken) + if err != nil { + l.ctx.Log.Errorf("couldn't get authorization url: %v", err) + return "", nil, ErrInternalError + } + + return account.ID.String(), authUrl, nil +} + +func (l *Logic) twitterLoginResolve(account *models.Account, pin string) error { + oauth1Config := l.getTwitterOAuthConfig() + + accessToken, accessSecret, err := oauth1Config.AccessToken(*account.RequestToken, *account.RequestSecret, pin) + if err != nil { + l.ctx.Log.Errorf("couldn't get access token: %v", err) + return ErrInternalError + } + + account.AccessToken = &accessToken + account.AccessTokenSecret = &accessSecret + account.RequestToken = nil + account.RequestSecret = nil + + twitterClient := l.getAcountClient(account) + + accountVerifyParams := &twitter.AccountVerifyParams{ + IncludeEntities: twitter.Bool(false), + SkipStatus: twitter.Bool(true), + IncludeEmail: twitter.Bool(false), + } + user, _, err := twitterClient.Accounts.VerifyCredentials(accountVerifyParams) + if err != nil { + l.ctx.Log.Errorf("couldn't verify credentials: %v", err) + return ErrInternalError + } + + account.TwitterID = &user.ID + account.TwitterHandle = user.Name + account.ScreenName = user.ScreenName + account.AvatarURL = user.ProfileImageURL + + err = l.ctx.Data.UpdateAccount(account) + if err != nil { + l.ctx.Log.Errorf("couldn't update account in database: %v", err) + return ErrInternalError + } + + return nil +} diff --git a/backend/internal/presentation/account.go b/backend/internal/presentation/account.go index 037eef9..34562b1 100644 --- a/backend/internal/presentation/account.go +++ b/backend/internal/presentation/account.go @@ -1,6 +1,9 @@ package presentation -import "threadule/backend/internal/web" +import ( + "threadule/backend/internal/presentation/dto" + "threadule/backend/internal/web" +) func GetAccounts(ctx *web.Context) { accounts, err := ctx.AppCtx.Logic.GetAccounts(ctx.Session.User) @@ -15,3 +18,43 @@ func GetAccounts(ctx *web.Context) { return } } + +func AddAccount(ctx *web.Context) { + id, url, err := ctx.AppCtx.Logic.AddAccount(ctx.Session.User) + if err != nil { + ErrorResponse(ctx, err) + return + } + + err = ctx.WriteJSON(&dto.AddAccountResponse{ + ID: id, + URL: url.String(), + }) + if err != nil { + ErrorResponse(ctx, err) + return + } +} + +func AddAccountResolve(ctx *web.Context) { + id := ctx.Params.ByName("id") + + var param dto.AddAccountResolveParam + err := ctx.ReadJSON(¶m) + if err != nil { + ErrorResponse(ctx, err) + return + } + + account, err := ctx.AppCtx.Logic.AddAccountResolve(ctx.Session.User, id, param.Pin) + if err != nil { + ErrorResponse(ctx, err) + return + } + + err = ctx.WriteJSON(account) + if err != nil { + ErrorResponse(ctx, err) + return + } +} diff --git a/backend/internal/presentation/dto/account.go b/backend/internal/presentation/dto/account.go new file mode 100644 index 0000000..3b2175f --- /dev/null +++ b/backend/internal/presentation/dto/account.go @@ -0,0 +1,10 @@ +package dto + +type AddAccountResponse struct { + URL string `json:"url"` + ID string `json:"id"` +} + +type AddAccountResolveParam struct { + Pin string `json:"pin"` +} diff --git a/backend/internal/presentation/responses.go b/backend/internal/presentation/responses.go index 15481fa..a5c748a 100644 --- a/backend/internal/presentation/responses.go +++ b/backend/internal/presentation/responses.go @@ -24,6 +24,8 @@ func StatusResponse(ctx *web.Context, status int, details string) { func ErrorResponse(ctx *web.Context, err error) { switch err { + case logic.ErrInvalidParameter: + StatusResponse(ctx, http.StatusBadRequest, err.Error()) case logic.ErrLoginFailed: StatusResponse(ctx, http.StatusForbidden, err.Error()) case logic.ErrInvalidSession: @@ -34,3 +36,8 @@ func ErrorResponse(ctx *web.Context, err error) { StatusResponse(ctx, http.StatusInternalServerError, err.Error()) } } + +func RedirectResponse(ctx *web.Context, url string) { + ctx.Response.Header().Add("Location", url) + ctx.Response.WriteHeader(http.StatusFound) +} diff --git a/backend/internal/router/routes.go b/backend/internal/router/routes.go index 6baa5e1..c991b0f 100644 --- a/backend/internal/router/routes.go +++ b/backend/internal/router/routes.go @@ -14,6 +14,8 @@ func Setup(ctx *app.Context) http.Handler { router.GET("/authentication", authenticated(GetAuthenticationData)) router.GET("/account/", authenticated(GetAccounts)) + router.POST("/account/", authenticated(AddAccount)) + router.POST("/account/:id", authenticated(AddAccountResolve)) return router }