feat(oauth): add twitter oauth support
This commit is contained in:
2
.example.env
Normal file
2
.example.env
Normal file
@@ -0,0 +1,2 @@
|
||||
TWITTER_CLIENT_ID=**********************************
|
||||
TWITTER_CLIENT_SECRET=**************************************************
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ Session.vim
|
||||
.vercel
|
||||
*_templ.go
|
||||
*_templ.txt
|
||||
.env
|
||||
dist
|
||||
tmp
|
||||
bin
|
||||
|
||||
23
components/logins.templ
Normal file
23
components/logins.templ
Normal 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
154
flake.lock
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
34
main.go
@@ -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)
|
||||
|
||||
4
makefile
4
makefile
@@ -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,
|
||||
|
||||
@@ -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
95
pages/twitter_login.templ
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user