feat(accounts): login with twitter works now

I'm f-ing tired.
This commit is contained in:
overflowerror 2021-08-14 22:29:24 +02:00
parent 7afad69041
commit 87aab8b23b
14 changed files with 241 additions and 19 deletions

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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).

View file

@ -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
}

View file

@ -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 {

View file

@ -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")
)

View file

@ -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

View file

@ -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
}

View file

@ -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(&param)
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
}
}

View file

@ -0,0 +1,10 @@
package dto
type AddAccountResponse struct {
URL string `json:"url"`
ID string `json:"id"`
}
type AddAccountResolveParam struct {
Pin string `json:"pin"`
}

View file

@ -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)
}

View file

@ -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
}