feat(oauth): add twitter oauth support

This commit is contained in:
Gustavo "Guz" L. de Mello
2024-06-27 19:53:09 -03:00
parent 1bc25628c3
commit 37fd862590
10 changed files with 312 additions and 36 deletions

2
.example.env Normal file
View File

@@ -0,0 +1,2 @@
TWITTER_CLIENT_ID=**********************************
TWITTER_CLIENT_SECRET=**************************************************

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ Session.vim
.vercel
*_templ.go
*_templ.txt
.env
dist
tmp
bin

23
components/logins.templ Normal file
View File

@@ -0,0 +1,23 @@
package components
import (
"fmt"
"os"
"net/url"
)
var loginUrl = fmt.Sprintf("https://x.com/i/oauth2/authorize"+
"?response_type=code"+
"&client_id=%s"+
"&redirect_uri=%s"+
"&scope=tweet.write tweet.read users.read"+
"&state=state"+
"&code_challenge=challenge"+
"&code_challenge_method=plain",
os.Getenv("TWITTER_CLIENT_ID"),
url.PathEscape("http://localhost:7331/api/oauth/twitter"),
)
templ LoginTwitter() {
<a href={ templ.SafeURL(loginUrl) } rel="">Login on Twitter</a>
}

154
flake.lock generated
View File

@@ -1,5 +1,81 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1710154385,
"narHash": "sha256-4c3zQ2YY4BZOufaBJB4v9VBBeN2dH7iVdoJw8SDNCfI=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "872b63ddd28f318489c929d25f1f0a3c6039c971",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1719075281,
@@ -16,9 +92,85 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1710565619,
"narHash": "sha256-xu/EnZCNdIj7m/QjCNIG5vrCA4TYg5uwFReb9XDxET0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8ac30a39abc5ea67037dfbf090d6e89f187c6e50",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"templ": "templ"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"templ": {
"inputs": {
"gitignore": "gitignore",
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs_2",
"xc": "xc"
},
"locked": {
"lastModified": 1716625173,
"narHash": "sha256-4TkK8zeoWWGmcBg8YwALo2EyKfOyq5ut/3TjG81a+8M=",
"owner": "a-h",
"repo": "templ",
"rev": "0d42d67413c2a0fa357018d1b1c0301231f3b359",
"type": "github"
},
"original": {
"owner": "a-h",
"ref": "v0.2.707",
"repo": "templ",
"type": "github"
}
},
"xc": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"templ",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709043057,
"narHash": "sha256-F/u9u1vevCUqgCz0WjcCOPcN+h9kLEP73nBqie86J2w=",
"owner": "joerdav",
"repo": "xc",
"rev": "52cfa7e7f2d5a0d97b45a6f69ff1403a901e78c6",
"type": "github"
},
"original": {
"owner": "joerdav",
"repo": "xc",
"type": "github"
}
}
},

View File

@@ -2,8 +2,9 @@
description = "learning.rs";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
templ.url = "github:a-h/templ?ref=v0.2.707";
};
outputs = { nixpkgs, ... }:
outputs = { nixpkgs, ... } @ inputs:
let
systems = [
"x86_64-linux"
@@ -16,6 +17,7 @@
pkgs = import nixpkgs { inherit system; };
in
f system pkgs);
templ = system: inputs.templ.packages.${system}.templ;
in
{
devShells = forAllSystems (system: pkgs: {
@@ -23,6 +25,7 @@
buildInputs = with pkgs; [
go
golangci-lint
(templ system)
];
};
});

View File

@@ -1,9 +1,32 @@
package internals
import (
"fmt"
"net/http"
"slices"
)
func GetCookie(name string, w http.ResponseWriter, r *http.Request) *http.Cookie {
name = fmt.Sprintf("__Host-%s-%s-%s", APP_NAME, APP_VERSION, name)
c := r.Cookies()
i := slices.IndexFunc(c, func(c *http.Cookie) bool {
return c.Name == name
})
var cookie *http.Cookie
if i == -1 {
cookie = &http.Cookie{
Name: name,
SameSite: http.SameSiteStrictMode,
Path: "/",
Secure: true,
}
} else {
cookie = c[i]
}
return cookie
}
func HttpErrorHelper(w http.ResponseWriter) func(msg string, err error, status int) bool {
return func(msg string, err error, status int) bool {
if err != nil {

34
main.go
View File

@@ -5,11 +5,9 @@ import (
"fmt"
"log"
"net/http"
"slices"
"strings"
"extrovert/config"
"extrovert/internals"
"extrovert/pages"
)
var logger = log.Default()
@@ -28,41 +26,13 @@ func main() {
mux := http.NewServeMux()
config.APIROUTES(mux)
for _, route := range config.ROUTES {
path := "/" + strings.TrimSuffix(route.Path, ".html")
if path == "/index" {
continue
}
logger.Printf("Registering page route. page=%s route=%s", route.Path, path)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
err := route.Component.Render(r.Context(), w)
if err != nil {
logger.Fatalf("Unable to render route %s due to %s", route.Path, err)
}
})
}
pages.RegisterAllRoutes(pages.ROUTES, mux)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
logger.Printf("Handling file server request. path=%s", r.URL.Path)
http.FileServer(http.Dir(*staticDir)).ServeHTTP(w, r)
return
}
w.Header().Add("Content-Type", "text/html")
index := slices.IndexFunc(config.ROUTES, func(route internals.Page) bool {
return route.Path == "index.html"
})
indexPage := config.ROUTES[index]
err := indexPage.Component.Render(r.Context(), w)
if err != nil {
log.Fatalf("Unable to render index page due to %s", err)
}
})
logger.Printf("Running server at port: %v", *port)

View File

@@ -34,7 +34,8 @@ dev/sync_assets:
--build.include_ext "js,css"
dev:
make -j3 dev/templ dev/server dev/sync_assets
go run github.com/joho/godotenv/cmd/godotenv@v1.5.1 \
make -j3 dev/templ dev/server dev/sync_assets
run: build
./bin/www
@@ -45,7 +46,6 @@ clean:
if [[ -d "dist" ]]; then rm -r ./dist; fi
if [[ -d "tmp" ]]; then rm -r ./tmp; fi
if [[ -d "bin" ]]; then rm -r ./bin; fi
rm ./static/uno.css
rm $(TEMPL_FILES)
# For some reason "templ generate" does not detect the files in CI,

View File

@@ -13,6 +13,13 @@ var ROUTES = []Route{
Page: IndexPage(),
Handler: IndexHandler,
},
{
Pattern: "/api/oauth/twitter",
Static: false,
Page: TwitterLogin(),
Handler: TwitterLoginHandler,
},
}
type RouteHandler = func(http.ResponseWriter, *http.Request)

95
pages/twitter_login.templ Normal file
View File

@@ -0,0 +1,95 @@
package pages
import (
"net/http"
"fmt"
"encoding/json"
"errors"
"os"
"extrovert/layouts"
"extrovert/internals"
)
type TwitterTokenResponse struct {
Type string `json:"token_type"`
Token string `json:"access_token"`
Expires int `json:"expires_in"`
Scope string `json:"scope"`
}
func TwitterLoginHandler(w http.ResponseWriter, r *http.Request) {
error := internals.HttpErrorHelper(w)
code := r.URL.Query().Get("code")
if code == "" {
error(
"Bad request",
errors.New("Missing \"code\" parameter"),
http.StatusBadRequest,
)
return
}
tReq := fmt.Sprintf("https://api.twitter.com/2/oauth2/token"+
"?grant_type=authorization_code"+
"&client_id=%s"+
"&code_verifier=challenge"+
"&code=%s"+
"&challenge_method=plain"+
"&redirect_uri=http://localhost:7331/api/oauth/twitter",
os.Getenv("TWITTER_CLIENT_ID"),
code,
)
t, err := http.Post(tReq, "application/x-www-form-urlencoded", bytes.NewReader([]byte("")))
if error("Error trying to request token from twitter", err, http.StatusInternalServerError) {
return
}
b, err := io.ReadAll(t.Body)
if error("Error trying to read response body from twitter", err, http.StatusInternalServerError) {
return
} else if t.StatusCode < 200 || t.StatusCode > 299 {
error(
"Error trying to request token from twitter, returned non-200 code",
errors.New(fmt.Sprintf("Code: %v, Return value: %s", t.StatusCode, string(b))),
http.StatusInternalServerError,
)
return
}
var res TwitterTokenResponse
err = json.Unmarshal(b, &res)
if error("Error trying to parse response body from twitter", err, http.StatusInternalServerError) {
return
}
c := internals.GetCookie("twitter-data", w, r)
c.Value = res.Token
http.SetCookie(w, c)
TwitterLogin().Render(context.Background(), w)
}
templ TwitterLogin() {
@layouts.Page("Project Extrovert") {
<dialog open>
<article>
<header>
<p>Logged into Twitter!</p>
</header>
<p>
Your account was succefully connected with project-extrovert!
Click "Ok" to return to the index page.
</p>
<footer>
<a href="/index.html">
<button>Ok</button>
</a>
</footer>
</article>
</dialog>
@IndexPage()
}
}