changeset 12:aaf85ae1f942

add very simple html template
author Dennis C. M. <dennis@denniscm.com>
date Thu, 20 Mar 2025 11:12:21 +0000
parents 6d91c612310a
children e7ab74d2ad88
files README.md api/handlers.go api/store.go app/handlers.go auth/auth.go auth/types.go bot/timer.go event/events.go event/types.go main.go socket/conn.go socket/types.go store/store.go www/blocks/head.html www/pages/index.html www/static/style.css
diffstat 16 files changed, 298 insertions(+), 226 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Sat Mar 15 17:08:03 2025 +0000
+++ b/README.md	Thu Mar 20 11:12:21 2025 +0000
@@ -1,13 +1,1 @@
 # pacobot
-
-## Instructions
-
-### Create a `.config.json` file with the following fields.
-
-```json
-{
-    "client_id": "",
-    "client_secret": "",
-    "broadcaster_user_id": ""
-}
-```
--- a/api/handlers.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/api/handlers.go	Thu Mar 20 11:12:21 2025 +0000
@@ -5,27 +5,28 @@
 
 	"github.com/denniscmcom/pacobot/auth"
 	"github.com/denniscmcom/pacobot/socket"
+	"github.com/denniscmcom/pacobot/store"
 	"github.com/gin-gonic/gin"
 )
 
-func GetUserHandler(c *gin.Context) {
+func GetUser(c *gin.Context) {
 	userName := c.Query("username")
-	user := auth.GetUser(userName, getAccessToken())
+	user := auth.GetUser(userName, store.GetAccessToken())
 
 	c.JSON(http.StatusOK, gin.H{
 		"message": user.Data[len(user.Data)-1].Id,
 	})
 }
 
-func AuthHandler(c *gin.Context) {
+func Auth(c *gin.Context) {
 	authUrl := auth.GetAuthUrl()
 	c.Redirect(http.StatusMovedPermanently, authUrl)
 }
 
-func AuthValidateHandler(c *gin.Context) {
+func AuthValidate(c *gin.Context) {
 	msg := "failed"
 
-	if auth.IsAuthTokenValid(getAccessToken()) {
+	if auth.IsAuthTokenValid(store.GetAccessToken()) {
 		msg = "ok"
 	}
 
@@ -34,37 +35,34 @@
 	})
 }
 
-func AuthRefreshHandler(c *gin.Context) {
-	authRes := auth.RefreshAuthToken(getAccessToken(), getRefreshToken())
-	setAccessToken(authRes.AccessToken)
-	setRefreshToken(authRes.RefreshToken)
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
-}
-
-func AuthRevokeHandler(c *gin.Context) {
-	auth.RevokeAuthToken(getAccessToken())
+func AuthRefresh(c *gin.Context) {
+	authRes := auth.RefreshAuthToken(store.GetAccessToken(), store.GetRefreshToken())
+	store.SetAccessToken(authRes.AccessToken)
+	store.SetRefreshToken(authRes.RefreshToken)
 
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
 }
 
-func TwitchCallbackHandler(c *gin.Context) {
-	authCode := c.Query("code")
-	authRes := auth.GetAuthToken(authCode)
-	authStore.Store("accessToken", authRes.AccessToken)
-	authStore.Store("refreshToken", authRes.RefreshToken)
+func AuthRevoke(c *gin.Context) {
+	auth.RevokeAuthToken(store.GetAccessToken())
 
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
 	})
 }
 
-func ConnectHandler(c *gin.Context) {
-	go socket.Connect(getAccessToken())
+func Twitch(c *gin.Context) {
+	authCode := c.Query("code")
+	authRes := auth.GetAuthToken(authCode)
+	store.SetAccessToken(authRes.AccessToken)
+	store.SetRefreshToken(authRes.RefreshToken)
+	c.Redirect(http.StatusFound, "/")
+}
+
+func Connect(c *gin.Context) {
+	go socket.Connect(store.GetAccessToken())
 
 	c.JSON(http.StatusOK, gin.H{
 		"message": "ok",
--- a/api/store.go	Sat Mar 15 17:08:03 2025 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-package api
-
-import (
-	"log"
-	"sync"
-)
-
-var authStore sync.Map
-
-func setAccessToken(accessToken string) {
-	authStore.Store("accessToken", accessToken)
-}
-
-func setRefreshToken(refreshToken string) {
-	authStore.Store("refreshToken", refreshToken)
-}
-
-func getAccessToken() string {
-	value, exists := authStore.Load("accessToken")
-
-	if !exists {
-		log.Fatal("api: access token not found")
-	}
-
-	return value.(string)
-}
-
-func getRefreshToken() string {
-	value, exists := authStore.Load("refreshToken")
-
-	if !exists {
-		log.Fatal("api: refresh token not found")
-	}
-
-	return value.(string)
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/handlers.go	Thu Mar 20 11:12:21 2025 +0000
@@ -0,0 +1,15 @@
+package app
+
+import (
+	"net/http"
+
+	"github.com/denniscmcom/pacobot/store"
+	"github.com/gin-gonic/gin"
+)
+
+func Index(c *gin.Context) {
+	c.HTML(http.StatusOK, "index.html", gin.H{
+		"title":    "Pacobot",
+		"isLogged": store.IsAccessTokenSet(),
+	})
+}
--- a/auth/auth.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/auth/auth.go	Thu Mar 20 11:12:21 2025 +0000
@@ -7,8 +7,6 @@
 	"net/url"
 )
 
-// TODO: Change unmarshall to JSON DECODE
-
 func GetAuthUrl() string {
 	config := ReadConfig()
 
@@ -21,7 +19,7 @@
 	params := url.Values{}
 	params.Add("client_id", config.ClientId)
 	params.Add("force_verify", "true")
-	params.Add("redirect_uri", "http://localhost:8080/twitch-auth-code-callback")
+	params.Add("redirect_uri", "http://localhost:8080/api/auth/twitch")
 	params.Add("response_type", "code")
 	params.Add("scope", "channel:bot user:read:chat")
 	params.Add("state", "c3ab8aa609ea11e793ae92361f002671")
@@ -31,7 +29,7 @@
 	return baseUrl.String()
 }
 
-func GetAuthToken(authCode string) AuthRes {
+func GetAuthToken(authCode string) Auth {
 	config := ReadConfig()
 
 	baseUrl := &url.URL{
@@ -45,7 +43,7 @@
 	formData.Add("client_secret", config.ClientSecret)
 	formData.Add("code", authCode)
 	formData.Add("grant_type", "authorization_code")
-	formData.Add("redirect_uri", "http://localhost:8080/twitch-auth-code-callback")
+	formData.Add("redirect_uri", "http://localhost:8080/api/auth/twitch")
 
 	res, err := http.PostForm(baseUrl.String(), formData)
 
@@ -59,7 +57,7 @@
 		log.Fatal("GetAuthToken")
 	}
 
-	var authRes AuthRes
+	var authRes Auth
 
 	err = json.NewDecoder(res.Body).Decode(&authRes)
 
@@ -119,7 +117,7 @@
 	defer res.Body.Close()
 }
 
-func RefreshAuthToken(authToken, refreshToken string) AuthRes {
+func RefreshAuthToken(authToken, refreshToken string) Auth {
 	config := ReadConfig()
 
 	baseUrl := &url.URL{
@@ -142,7 +140,7 @@
 
 	defer res.Body.Close()
 
-	var authRes AuthRes
+	var authRes Auth
 
 	err = json.NewDecoder(res.Body).Decode(&authRes)
 
@@ -153,7 +151,7 @@
 	return authRes
 }
 
-func GetUser(userName, authToken string) UserRes {
+func GetUser(userName, authToken string) User {
 	config := ReadConfig()
 
 	baseUrl := &url.URL{
@@ -183,7 +181,7 @@
 
 	defer res.Body.Close()
 
-	var userRes UserRes
+	var userRes User
 
 	err = json.NewDecoder(res.Body).Decode(&userRes)
 
--- a/auth/types.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/auth/types.go	Thu Mar 20 11:12:21 2025 +0000
@@ -6,13 +6,13 @@
 	BroadcasterUserId string `json:"broadcaster_user_id"`
 }
 
-type UserRes struct {
+type User struct {
 	Data []struct {
 		Id string `json:"id"`
 	} `json:"data"`
 }
 
-type AuthRes struct {
+type Auth struct {
 	AccessToken  string   `json:"access_token"`
 	RefreshToken string   `json:"refresh_token"`
 	Scope        []string `json:"scope"`
--- a/bot/timer.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/bot/timer.go	Thu Mar 20 11:12:21 2025 +0000
@@ -17,7 +17,7 @@
 	quit = make(chan struct{})
 
 	go func() {
-		filename := "F:/Media/Twitch/Bot/timer.txt"
+		filename := "timer.txt"
 		file, err := os.Create(filename)
 
 		if err != nil {
--- a/event/events.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/event/events.go	Thu Mar 20 11:12:21 2025 +0000
@@ -10,7 +10,7 @@
 	"github.com/denniscmcom/pacobot/auth"
 )
 
-func ChannelChatMsgSub(authToken, session_id string) {
+func SubChannelChatMsg(authToken, session_id string) {
 	config := auth.ReadConfig()
 
 	data := map[string]any{
@@ -33,10 +33,10 @@
 	}
 
 	log.Printf("event: subscribing to %s", data["type"])
-	eventSub(authToken, jsonData)
+	subEvent(authToken, jsonData)
 }
 
-func eventSub(authToken string, subData []byte) {
+func subEvent(authToken string, subData []byte) {
 	baseUrl := &url.URL{
 		Scheme: "https",
 		Host:   "api.twitch.tv",
--- a/event/types.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/event/types.go	Thu Mar 20 11:12:21 2025 +0000
@@ -1,6 +1,6 @@
 package event
 
-type ChannelChatMsgSubPayload struct {
+type ChannelChatMsgSub struct {
 	Payload struct {
 		Event struct {
 			ChatterUserId    string `json:"chatter_user_id"`
--- a/main.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/main.go	Thu Mar 20 11:12:21 2025 +0000
@@ -2,6 +2,7 @@
 
 import (
 	"github.com/denniscmcom/pacobot/api"
+	"github.com/denniscmcom/pacobot/app"
 	"github.com/gin-gonic/gin"
 )
 
@@ -9,14 +10,26 @@
 	gin.SetMode(gin.DebugMode)
 
 	r := gin.Default()
+	r.LoadHTMLGlob("./www/**/*.html")
+	r.Static("/static", "./www/static")
 
-	r.GET("/user", api.GetUserHandler)
-	r.GET("/auth", api.AuthHandler)
-	r.GET("/auth-validate", api.AuthValidateHandler)
-	r.GET("/auth-refresh", api.AuthRefreshHandler)
-	r.GET("/auth-revoke", api.AuthRevokeHandler)
-	r.GET("/twitch-auth-code-callback", api.TwitchCallbackHandler)
-	r.GET("/connect", api.ConnectHandler)
+	r.GET("/", app.Index)
+
+	{
+		apiG := r.Group("/api")
+		apiG.GET("/user", api.GetUser)
+		apiG.GET("/connect", api.Connect)
+
+		{
+			authG := apiG.Group("/auth")
+			authG.GET("/", api.Auth)
+			authG.GET("/validate", api.AuthValidate)
+			authG.GET("/refresh", api.AuthRefresh)
+			authG.GET("/revoke", api.AuthRevoke)
+			authG.GET("/twitch", api.Twitch)
+		}
+
+	}
 
 	r.Run()
 }
--- a/socket/conn.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/socket/conn.go	Thu Mar 20 11:12:21 2025 +0000
@@ -69,59 +69,60 @@
 			break
 		}
 
-		var resMetadata Res_Metadata
+		var metadataMsg MetadataMsg
 
-		if err := json.Unmarshal(msg, &resMetadata); err != nil {
+		if err := json.Unmarshal(msg, &metadataMsg); err != nil {
 			log.Fatal(err)
 		}
 
-		msgType := resMetadata.Metadata.MsgType
+		msgType := metadataMsg.Metadata.MsgType
 		log.Printf("socket: %s msg received", msgType)
 
 		switch msgType {
 		case "session_welcome":
-			var resWelcome Res_Welcome
+			var welcomeMsg WelcomeMsg
 
-			if err := json.Unmarshal(msg, &resWelcome); err != nil {
+			if err := json.Unmarshal(msg, &welcomeMsg); err != nil {
 				log.Fatal(err)
 			}
 
-			timeout_secs = time.Duration(resWelcome.Payload.Session.KeepaliveTimeout+3) * time.Second
+			timeout_secs = time.Duration(welcomeMsg.Payload.Session.KeepaliveTimeoutSecs+3) * time.Second
 			timeout = time.NewTicker(timeout_secs)
 			defer timeout.Stop()
 
-			event.ChannelChatMsgSub(authToken, resWelcome.Payload.Session.Id)
+			event.SubChannelChatMsg(authToken, welcomeMsg.Payload.Session.Id)
 
 		case "session_keepalive":
 			timeout.Reset(timeout_secs)
 			log.Println("socket: timeout resetted")
 
 		case "notification":
-			var resMetadataNotif Res_Metadata_Notif
+			var metadataEvent MetadataEvent
 
-			if err := json.Unmarshal(msg, &resMetadataNotif); err != nil {
+			if err := json.Unmarshal(msg, &metadataEvent); err != nil {
 				log.Fatal(err)
 			}
 
-			subType := resMetadataNotif.Metadata.SubType
+			subType := metadataEvent.Metadata.SubType
 			log.Printf("socket: %s event received", subType)
 
 			switch subType {
 			case "channel.chat.message":
-				var resNotifChannelChatMsg Res_Notif_ChannelChatMsg
+				var channelChatMsgEvent ChannelChatMsgEvent
 
-				if err := json.Unmarshal(msg, &resNotifChannelChatMsg); err != nil {
+				if err := json.Unmarshal(msg, &channelChatMsgEvent); err != nil {
 					log.Fatal(err)
 				}
 
-				chatMsg := resNotifChannelChatMsg.Payload.Event.Msg.Text
+				chatMsg := channelChatMsgEvent.Payload.Event.Msg.Text
 
 				if strings.HasPrefix(chatMsg, "!") {
-					if resNotifChannelChatMsg.Payload.Event.ChatterUserName == "denniscmartin" {
+					if channelChatMsgEvent.Payload.Event.ChatterUserName == "denniscmartin" {
 						go bot.HandleCmd(strings.Split(chatMsg[1:], " "))
 					}
 				}
 			}
+
 		default:
 			log.Fatalf("socket: %s message type not implemented", msgType)
 		}
@@ -139,24 +140,3 @@
 
 	log.Println("socket: connection closed")
 }
-
-// func test() {
-// 	var res Response
-
-// 	// Deserializas
-// 	err := json.Unmarshal([]byte(jsonData), &res)
-
-// 	if err != nil {
-// 		fmt.Println("Error al deserializar:", err)
-// 		return
-// 	}
-
-// 	// Conviertes la estructura nuevamente a JSON formateado
-
-// 	formattedJSON, err := json.MarshalIndent(res, "", " ")
-
-// 	if err != nil {
-// 		fmt.Println("Error al formatear JSON:", err)
-// 		return
-// 	}
-// }
--- a/socket/types.go	Sat Mar 15 17:08:03 2025 +0000
+++ b/socket/types.go	Thu Mar 20 11:12:21 2025 +0000
@@ -1,117 +1,118 @@
 package socket
 
-type Res_Metadata struct {
-	Metadata Metadata `json:"metadata"`
-}
-
-type Res_Metadata_Notif struct {
-	Metadata Metadata_Notif `json:"metadata"`
-}
-
-type Res_Welcome struct {
-	Metadata Metadata        `json:"metadata"`
-	Payload  Payload_Welcome `json:"payload"`
-}
-
-type Res_Keepalive struct {
-	Metadata Metadata          `json:"metadata"`
-	Payload  Payload_Keepalive `json:"payload"`
-}
-
-type Res_Notif_ChannelChatMsg struct {
-	Metadata Metadata_Notif               `json:"metadata,omitempty"`
-	Payload  Payload_Notif_ChannelChatMsg `json:"payload,omitempty"`
+type MetadataMsg struct {
+	Metadata struct {
+		MsgId        string `json:"message_id"`
+		MsgType      string `json:"message_type"`
+		MsgTimeStamp string `json:"message_timestamp"`
+	} `json:"metadata"`
 }
 
-type Metadata struct {
-	MsgId        string `json:"message_id"`
-	MsgType      string `json:"message_type"`
-	MsgTimestamp string `json:"message_timestamp"`
-}
+type WelcomeMsg struct {
+	Metadata struct {
+		MsgId        string `json:"message_id"`
+		MsgType      string `json:"message_type"`
+		MsgTimeStamp string `json:"message_timestamp"`
+	} `json:"metadata"`
 
-type Metadata_Notif struct {
-	Metadata
-	SubType    string `json:"subscription_type"`
-	SubVersion string `json:"subscription_version"`
+	Payload struct {
+		Session struct {
+			Id                   string `json:"id"`
+			Status               string `json:"status"`
+			ConnectedAt          string `json:"connected_at"`
+			KeepaliveTimeoutSecs int    `json:"keepalive_timeout_seconds"`
+			ReconnectUrl         string `json:"reconnect_url"`
+		} `json:"session"`
+	} `json:"payload"`
 }
 
-type Payload_Welcome struct {
-	Session struct {
-		Id               string `json:"id"`
-		Status           string `json:"status"`
-		ConnectedAt      string `json:"connected_at"`
-		KeepaliveTimeout int    `json:"keepalive_timeout_seconds"`
-	} `json:"session"`
+type KeealiveMsg struct {
+	Metadata struct {
+		MsgId        string `json:"message_id"`
+		MsgType      string `json:"message_type"`
+		MsgTimeStamp string `json:"message_timestamp"`
+	} `json:"metadata"`
+
+	Payload struct {
+	} `json:"payload"`
 }
 
-type Payload_Keepalive struct {
+type ReconnectMsg struct {
+	WelcomeMsg
 }
 
-type Payload_Notif_ChannelChatMsg struct {
-	Subscription Payload_Sub_Notif_ChannelChatMsg   `json:"subscription"`
-	Event        Payload_Event_Notif_ChannelChatMsg `json:"event"`
+type MetadataEvent struct {
+	Metadata struct {
+		MsgId        string `json:"message_id"`
+		MsgType      string `json:"message_type"`
+		MsgTimeStamp string `json:"message_timestamp"`
+		SubType      string `json:"subscription_type"`
+		SubVersion   string `json:"subscription_version"`
+	} `json:"metadata"`
 }
 
-type Payload_Sub_Notif struct {
-	Id      string `json:"id"`
-	Status  string `json:"status"`
-	Type    string `json:"type"`
-	Version string `json:"version"`
-	Cost    int    `json:"cost"`
+type ChannelChatMsgEvent struct {
+	Metadata MetadataEvent `json:"metadata"`
 
-	Transport struct {
-		Method    string `json:"method"`
-		SessionId string `json:"session_id"`
-	} `json:"transport"`
+	Payload struct {
+		Sub struct {
+			Id      string `json:"id"`
+			Status  string `json:"status"`
+			Type    string `json:"type"`
+			Version string `json:"version"`
+
+			Condition struct {
+				BroadcasterUserId string `json:"broadcaster_user_id"`
+				UserId            string `json:"user_id"`
+			} `json:"condition"`
 
-	CreatedAt string `json:"created_at"`
-}
+			Transport struct {
+				Method    string `json:"method"`
+				SessionId string `json:"session_id"`
+			} `json:"transport"`
 
-type Payload_Sub_Notif_ChannelChatMsg struct {
-	Payload_Sub_Notif
+			CreatedAt string `json:"created_at"`
+			Cost      int    `json:"cost"`
+		} `json:"subscription"`
 
-	Condition struct {
-		BroadcasterUserId string `json:"broadcaster_user_id"`
-		UserId            string `json:"user_id"`
-	} `json:"condition"`
-}
+		Event struct {
+			BroadcasterUserId    string `json:"broadcaster_user_id"`
+			BroadcasterUserLogin string `json:"broadcaster_user_login"`
+			BroadcasterUserName  string `json:"broadcaster_user_name"`
+			ChatterUserId        string `json:"chatter_user_id"`
+			ChatterUserLogin     string `json:"chatter_user_login"`
+			ChatterUserName      string `json:"chatter_user_name"`
+			MsgId                string `json:"message_id"`
 
-type Payload_Event_Notif_ChannelChatMsg struct {
-	BroadcasterUserId    string `json:"broadcaster_user_id"`
-	BroadcasterUserLogin string `json:"broadcaster_user_login"`
-	BroadcasterUserName  string `json:"broadcaster_user_name"`
-	ChatterUserId        string `json:"chatter_user_id"`
-	ChatterUserLogin     string `json:"chatter_user_login"`
-	ChatterUserName      string `json:"chatter_user_name"`
-	MsgId                string `json:"message_id"`
+			Msg struct {
+				Text string `json:"text"`
 
-	Msg struct {
-		Text string `json:"text"`
+				Fragments []struct {
+					Type      string `json:"type"`
+					Text      string `json:"text"`
+					Cheermote string `json:"cheermote"`
+					Emote     string `json:"emote"`
+					Mention   string `json:"mention"`
+				} `json:"fragments"`
+			} `json:"message"`
+
+			Color string `json:"color"`
 
-		Fragments []struct {
-			Type      string `json:"type"`
-			Text      string `json:"text"`
-			Cheermote string `json:"cheermote"`
-			Emote     string `json:"emote"`
-			Mention   string `json:"mention"`
-		} `json:"fragments"`
-	} `json:"message"`
-
-	Color string `json:"color"`
+			Badges []struct {
+				SetId string `json:"set_id"`
+				Id    string `json:"id"`
+				Info  string `json:"info"`
+			} `json:"badges"`
 
-	Badges []struct {
-		SetId string `json:"set_id"`
-		Id    string `json:"id"`
-		Info  string `json:"info"`
-	} `json:"badges"`
-
-	MsgType                     string `json:"message_type"`
-	Cheer                       string `json:"cheer"`
-	Reply                       string `json:"reply"`
-	ChannelPointsCustomRewardId string `json:"channel_points_custom_reward_id"`
-	SourceBroadcasterUserId     string `json:"source_broadcaster_user_id"`
-	SourceBroadcasterUserLogin  string `json:"source_broadcaster_user_login"`
-	SourceBroadcasterUserName   string `json:"source_broadcaster_user_name"`
-	SourceMessageId             string `json:"source_message_id"`
-	SourceBadges                string `json:"source_badges"`
+			MsgType                     string `json:"message_type"`
+			Cheer                       string `json:"cheer"`
+			Reply                       string `json:"reply"`
+			ChannelPointsCustomRewardId string `json:"channel_points_custom_reward_id"`
+			SourceBroadcasterUserId     string `json:"source_broadcaster_user_id"`
+			SourceBroadcasterUserLogin  string `json:"source_broadcaster_user_login"`
+			SourceBroadcasterUserName   string `json:"source_broadcaster_user_name"`
+			SourceMessageId             string `json:"source_message_id"`
+			SourceBadges                string `json:"source_badges"`
+		} `json:"event"`
+	} `json:"payload"`
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/store/store.go	Thu Mar 20 11:12:21 2025 +0000
@@ -0,0 +1,42 @@
+package store
+
+import (
+	"log"
+	"sync"
+)
+
+var authStore sync.Map
+
+func SetAccessToken(accessToken string) {
+	authStore.Store("accessToken", accessToken)
+}
+
+func SetRefreshToken(refreshToken string) {
+	authStore.Store("refreshToken", refreshToken)
+}
+
+func GetAccessToken() string {
+	value, exists := authStore.Load("accessToken")
+
+	if !exists {
+		log.Fatal("api: access token not found")
+	}
+
+	return value.(string)
+}
+
+func GetRefreshToken() string {
+	value, exists := authStore.Load("refreshToken")
+
+	if !exists {
+		log.Fatal("api: refresh token not found")
+	}
+
+	return value.(string)
+}
+
+func IsAccessTokenSet() bool {
+	_, exists := authStore.Load("accessToken")
+
+	return exists
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/blocks/head.html	Thu Mar 20 11:12:21 2025 +0000
@@ -0,0 +1,6 @@
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link href="/static/style.css" rel="stylesheet">
+    <title>{{ .title }}</title>
+</head>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/pages/index.html	Thu Mar 20 11:12:21 2025 +0000
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    {{ template "head.html" .}}
+    <style>
+        .container {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+    </style>
+</head>
+
+<body>
+    <h1>Hola, soy Pacobot, tu bot de Twitch.</h1>
+    <div class="col">
+        <div class="container">
+            <a href="api/auth"><button id="loginBtn">Login</button></a>
+            <div id="loginOkMsg">{{ if .isLogged}} ✅ Logged in {{ end }}</div>
+        </div>
+        <div class="container">
+            <button id="connectBtn">Connect</button>
+            <div id="connectOkMsg" hidden>✅ Connected</div>
+            <div id="connectErrorMsg" hidden>❌ Connection failed</div>
+        </div>
+    </div>
+</body>
+
+<script lang="js">
+    const connectBtn = document.getElementById('connectBtn');
+    const connectOkMsg = document.getElementById("connectOkMsg")
+    const connectErrorMsg = document.getElementById("connectErrorMsg")
+
+    connectBtn.addEventListener('click', async () => {
+        try {
+            const response = await fetch('/api/connect');
+
+            if (!response.ok) {
+                throw new Error(`HTTP error! Status: ${response.status}`);
+            }
+
+            connectOkMsg.hidden = false;
+            connectErrorMsg.hidden = true;
+        } catch (error) {
+            console.error('Login failed:', error);
+            connectOkMsg.hidden = true;
+            connectErrorMsg.hidden = false;
+        }
+    });
+</script>
+
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/static/style.css	Thu Mar 20 11:12:21 2025 +0000
@@ -0,0 +1,14 @@
+body {
+    margin: auto;
+    width: 60%;
+    border: 3px solid gray;
+    padding: 20px;
+    font-family: Arial, sans-serif;
+    line-height: 1.6;
+}
+
+.col {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}