mirror of
https://github.com/sigmasternchen/threadule
synced 2025-03-15 08:09:01 +00:00
feat(accounts): login with twitter works now
I'm f-ing tired.
This commit is contained in:
parent
7afad69041
commit
87aab8b23b
14 changed files with 241 additions and 19 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
10
backend/internal/logic/error.go
Normal file
10
backend/internal/logic/error.go
Normal 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")
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
10
backend/internal/presentation/dto/account.go
Normal file
10
backend/internal/presentation/dto/account.go
Normal 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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue