93 lines
2.5 KiB
Go
93 lines
2.5 KiB
Go
|
|
package provider
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"apps/backend/internal/module/threads/domain"
|
||
|
|
)
|
||
|
|
|
||
|
|
// FakeProvider implements OAuth without Meta (dev + tests).
|
||
|
|
// AuthorizeURL points at callbackURL with code=fake&state=...
|
||
|
|
type FakeProvider struct {
|
||
|
|
// CallbackBase is full callback path e.g. http://127.0.0.1:8888/api/v1/threads-accounts/oauth/callback
|
||
|
|
CallbackBase string
|
||
|
|
// FailExchange forces TH-04
|
||
|
|
FailExchange bool
|
||
|
|
// FailRefresh forces TH-07
|
||
|
|
FailRefresh bool
|
||
|
|
// FixedUserID when set, Exchange always returns this Threads user id (reconnect tests)
|
||
|
|
FixedUserID string
|
||
|
|
// FixedUsername optional with FixedUserID
|
||
|
|
FixedUsername string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *FakeProvider) Name() string { return "fake" }
|
||
|
|
|
||
|
|
func (f *FakeProvider) AuthorizeURL(state, redirectURI string) string {
|
||
|
|
base := f.CallbackBase
|
||
|
|
if base == "" {
|
||
|
|
base = redirectURI
|
||
|
|
}
|
||
|
|
u, err := url.Parse(base)
|
||
|
|
if err != nil {
|
||
|
|
return base + "?code=fake&state=" + url.QueryEscape(state)
|
||
|
|
}
|
||
|
|
q := u.Query()
|
||
|
|
q.Set("code", "fake")
|
||
|
|
q.Set("state", state)
|
||
|
|
u.RawQuery = q.Encode()
|
||
|
|
return u.String()
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *FakeProvider) ExchangeCode(ctx context.Context, code, redirectURI string) (*domain.TokenPair, error) {
|
||
|
|
_ = ctx
|
||
|
|
_ = redirectURI
|
||
|
|
if f.FailExchange || code == "fail" {
|
||
|
|
return nil, domain.ErrOAuthExchange
|
||
|
|
}
|
||
|
|
if code != "fake" && !strings.HasPrefix(code, "fake") {
|
||
|
|
// still accept any code in pure fake mode for flexibility
|
||
|
|
}
|
||
|
|
id := f.FixedUserID
|
||
|
|
if id == "" {
|
||
|
|
id = fmt.Sprintf("fake_%d", domain.NowNano()%1_000_000)
|
||
|
|
}
|
||
|
|
user := f.FixedUsername
|
||
|
|
if user == "" {
|
||
|
|
user = "threads_" + id
|
||
|
|
}
|
||
|
|
return &domain.TokenPair{
|
||
|
|
AccessToken: "fake-access-" + id + fmt.Sprintf("_%d", domain.NowNano()%10000),
|
||
|
|
RefreshToken: "fake-refresh-" + id,
|
||
|
|
ExpiresIn: 3600 * 24 * 60, // ~60d
|
||
|
|
UserID: id,
|
||
|
|
Username: user,
|
||
|
|
DisplayName: "Threads " + user,
|
||
|
|
AvatarURL: "",
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *FakeProvider) Refresh(ctx context.Context, refreshToken string) (*domain.TokenPair, error) {
|
||
|
|
_ = ctx
|
||
|
|
if f.FailRefresh || refreshToken == "" || strings.Contains(refreshToken, "bad") {
|
||
|
|
return nil, domain.ErrRefreshFailed
|
||
|
|
}
|
||
|
|
id := "refreshed"
|
||
|
|
if strings.HasPrefix(refreshToken, "fake-refresh-") {
|
||
|
|
id = strings.TrimPrefix(refreshToken, "fake-refresh-")
|
||
|
|
}
|
||
|
|
return &domain.TokenPair{
|
||
|
|
AccessToken: "fake-access-" + id,
|
||
|
|
RefreshToken: refreshToken,
|
||
|
|
ExpiresIn: 3600 * 24 * 60,
|
||
|
|
UserID: id,
|
||
|
|
Username: "threads_" + id,
|
||
|
|
DisplayName: "Threads " + id,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
var _ domain.Provider = (*FakeProvider)(nil)
|