1package handler
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10 "time"
11
12 "github.com/bnema/gordon/internal/db"
13 "github.com/bnema/gordon/internal/db/queries"
14 "github.com/bnema/gordon/internal/server"
15 "github.com/bnema/gordon/internal/templating/render"
16 "github.com/gorilla/sessions"
17 _ "github.com/joho/godotenv/autoload"
18 "github.com/labstack/echo-contrib/session"
19 "github.com/labstack/echo/v4"
20)
21
22var urlToken string
23
24// ClLI :
25// compareGordonToken compares the token from the URL query parameter with the one from the config.yml
26func CompareGordonToken(c echo.Context, a *server.App) error {
27 configToken, err := a.Config.GetToken()
28 if err != nil {
29 return err
30 }
31
32 if urlToken != configToken {
33 return fmt.Errorf("token is empty or not valid")
34 }
35
36 return nil
37}
38
39// RenderLoginPage renders the login.html template
40func RenderLoginPage(c echo.Context, a *server.App) error {
41 urlToken = c.QueryParam("token")
42 data := map[string]interface{}{
43 "Title": "Login",
44 }
45
46 // Navigate inside the fs.FS to get the template
47 path := "html/login"
48 rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a)
49 if err != nil {
50 return err
51 }
52
53 renderedHTML, err := rendererData.Render(data, a)
54 if err != nil {
55 return err
56 }
57
58 return c.HTML(200, renderedHTML)
59}
60
61// StartOAuthGithub starts the Github OAuth flow.
62func StartOAuthGithub(c echo.Context, a *server.App) error {
63 fmt.Println("Attempting to get an existing session.")
64 sess, err := getSession(c)
65 if err != nil || sess == nil || sess.Values == nil {
66 fmt.Println("Session retrieval failed, initiating new GitHub OAuth flow.")
67 return initiateGithubOAuthFlow(c, a)
68 }
69
70 fmt.Println("Attempting to assert session values.")
71 sessionID, ok1 := sess.Values["sessionID"].(string)
72 accountID, ok2 := sess.Values["accountID"].(string)
73 authenticated, ok3 := sess.Values["authenticated"].(bool)
74
75 if !ok1 || !ok2 || !ok3 || !authenticated {
76 fmt.Println("Session assertion failed, or user not authenticated. Initiating new OAuth flow.")
77 return initiateGithubOAuthFlow(c, a)
78 }
79
80 fmt.Println("Checking if session exists in DB.")
81 sessionExists, err := queries.CheckDBSessionExists(a, sessionID)
82 if err != nil {
83 fmt.Printf("Error checking if session exists: %v\n", err)
84 return fmt.Errorf("could not check if session exists: %w", err)
85 }
86
87 if !sessionExists {
88 fmt.Println("Session does not exist in DB, initiating new OAuth flow.")
89 return initiateGithubOAuthFlow(c, a)
90 }
91
92 fmt.Println("Getting the expiration time of the session.")
93 currentTime := time.Now()
94 sessionExpiration, err := queries.GetSessionExpiration(a, accountID, sessionID, currentTime)
95 if err != nil {
96 fmt.Printf("Error getting session expiration: %v\n", err)
97 return fmt.Errorf("could not get session expiration: %w", err)
98 }
99
100 if currentTime.After(sessionExpiration) {
101 fmt.Println("Current time is after session expiration, initiating new OAuth flow.")
102 return initiateGithubOAuthFlow(c, a)
103 }
104
105 fmt.Println("Extending session expiration time by 30 minutes.")
106 newExpirationTime := currentTime.Add(30 * time.Minute)
107 err = queries.ExtendSessionExpiration(a, accountID, sessionID, newExpirationTime)
108 if err != nil {
109 fmt.Printf("Error extending session expiration: %v\n", err)
110 return fmt.Errorf("could not extend session expiration: %w", err)
111 }
112
113 fmt.Println("Session is valid and extended. Redirecting to admin panel.")
114 return c.Redirect(http.StatusFound, a.Config.Admin.Path)
115}
116
117// initiateGithubOAuthFlow starts a new GitHub OAuth flow
118func initiateGithubOAuthFlow(c echo.Context, a *server.App) error {
119 clientID := "" // IDK why it needs that even empty, but it does
120 redirectDomain := a.GenerateOauthCallbackURL()
121 encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
122
123 // Redirect to GitHub's OAuth endpoint to grab the OAuth access
124 proxyURL := a.Config.Build.ProxyURL
125 oauthURL := fmt.Sprintf(
126 "%s/github/authorize?client_id=%s&redirect_uri=%s&state=%s",
127 proxyURL,
128 clientID,
129 redirectDomain,
130 encodedState,
131 )
132 return c.Redirect(http.StatusFound, oauthURL)
133}
134
135type Sessions struct {
136 *db.Sessions
137}
138
139// OAuthCallback handles the callback response from Github OAuth
140func OAuthCallback(c echo.Context, a *server.App) error {
141 redirectPath := a.Config.Admin.Path
142
143 accessToken, encodedState, err := parseQueryParams(c)
144 if err != nil {
145 return c.String(http.StatusBadRequest, err.Error())
146 }
147
148 // update the struct with the new access token
149 a.DBTables.Sessions.AccessToken = accessToken
150
151 // Compare the state parameter with the redirect domain
152 redirectDomain := a.GenerateOauthCallbackURL()
153 encodedRedirectDomain := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
154 if encodedState != encodedRedirectDomain {
155 return c.String(http.StatusBadRequest, "invalid state parameter")
156 }
157
158 sess, err := getSession(c)
159 if err != nil {
160 return err
161 }
162
163 browserInfo := c.Request().UserAgent()
164 userInfo, err := githubGetUserDetails(c, a)
165 if err != nil {
166 return err
167 }
168
169 user, err := handleUser(c, a, accessToken, browserInfo, userInfo)
170 if err != nil {
171 return err
172 }
173
174 err = setSessionValues(c, sess, user, a.DBTables.Account.ID, a.DBTables.Sessions.Expires, a.DBTables.Sessions.ID)
175 if err != nil {
176 return err
177 }
178
179 return c.Redirect(http.StatusFound, redirectPath)
180}
181
182// parseQueryParams parses the query parameters from the callback URL
183func parseQueryParams(c echo.Context) (string, string, error) {
184 accessToken := c.QueryParam("access_token")
185 encodedState := c.QueryParam("state")
186
187 _, err := base64.StdEncoding.DecodeString(encodedState)
188 if err != nil {
189 return "", "", fmt.Errorf("invalid state parameter")
190 }
191
192 return accessToken, encodedState, nil
193}
194
195// getSession gets the session from the context
196func getSession(c echo.Context) (*sessions.Session, error) {
197 sess, err := session.Get("session", c)
198 if err != nil {
199 return nil, echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
200 }
201 return sess, nil
202}
203
204// handleUser handles the user creation or update
205func handleUser(c echo.Context, a *server.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
206 userExists, err := queries.CheckDBUserExists(a)
207 if err != nil {
208 return nil, fmt.Errorf("could not check if user exists: %w", err)
209 }
210
211 if !userExists {
212 // if it is a new user creation we compare the gordon token
213 err := CompareGordonToken(c, a)
214 if err != nil {
215 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is empty or not valid")
216 }
217
218 return createUser(a, accessToken, browserInfo, userInfo)
219 }
220
221 if userExists {
222 // if it is an existing user we compare the login and email to see if it is the same user
223 isGoodUser, err := queries.CheckDBUserIsGood(a, userInfo)
224 if err != nil {
225 return nil, fmt.Errorf("could not check if user is good: %w", err)
226 }
227
228 if !isGoodUser {
229 return nil, echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
230 }
231
232 if isGoodUser {
233 // Update the user
234 updatedUser, err := updateUser(a, accessToken, browserInfo, userInfo)
235 if err != nil {
236 return nil, fmt.Errorf("could not update user: %w", err)
237 }
238 return updatedUser, nil
239 }
240 }
241
242 return nil, fmt.Errorf("could not handle user")
243}
244
245// createUser creates a new user in the database
246func createUser(a *server.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
247 err := queries.CreateUser(a, accessToken, browserInfo, userInfo)
248 if err != nil {
249 return nil, fmt.Errorf("could not create user: %w", err)
250 }
251 return &a.DBTables.User, nil
252}
253
254// updateUser updates an existing user in the database
255func updateUser(a *server.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
256 user, err := queries.UpdateUser(a, accessToken, browserInfo, userInfo)
257 if err != nil {
258 return nil, fmt.Errorf("could not update user: %w", err)
259 }
260 return user, nil
261}
262
263// setSessionValues sets the session values for the user
264func setSessionValues(c echo.Context, sess *sessions.Session, user *db.User, accountID string, expires string, sessionID string) error {
265 sess.Values["authenticated"] = true
266 sess.Values["accountID"] = accountID
267 sess.Values["expires"] = expires
268 sess.Values["sessionID"] = sessionID
269
270 if err := sess.Save(c.Request(), c.Response()); err != nil {
271 return fmt.Errorf("could not save session: %w", err)
272 }
273 return nil
274}
275
276// fetchGithubAPI fetches the Github API
277func fetchGithubAPI(url string, authHeader string, result interface{}) error {
278 req, err := http.NewRequest("GET", url, nil)
279 if err != nil {
280 return err
281 }
282
283 req.Header.Set("Authorization", authHeader)
284
285 resp, err := http.DefaultClient.Do(req)
286 if err != nil {
287 return err
288 }
289 defer resp.Body.Close()
290
291 if resp.StatusCode != http.StatusOK {
292 return fmt.Errorf("got status code %d", resp.StatusCode)
293 }
294
295 body, err := io.ReadAll(resp.Body)
296 if err != nil {
297 return err
298 }
299
300 err = json.Unmarshal(body, &result)
301 if err != nil {
302 return err
303 }
304
305 return nil
306}
307
308// githubGetUserDetails gets the user details from the Github API using the access token
309func githubGetUserDetails(c echo.Context, a *server.App) (userInfo *queries.GithubUserInfo, err error) {
310 accessToken := c.QueryParam("access_token")
311
312 // Fetch user info
313 err = fetchGithubAPI("https://api.github.com/user", "Bearer "+accessToken, &userInfo)
314 if err != nil {
315 return nil, err
316 }
317 // Fetch user emails
318 var emailObjects []struct {
319 Email string `json:"email"`
320 }
321 err = fetchGithubAPI("https://api.github.com/user/emails", "Bearer "+accessToken, &emailObjects)
322 if err != nil {
323 return nil, err
324 }
325
326 // Extract emails and populate the Emails field in userInfo
327 for _, emailObj := range emailObjects {
328 userInfo.Emails = append(userInfo.Emails, emailObj.Email)
329 }
330
331 return userInfo, nil
332}
333
334func DeviceCodeRequest(c echo.Context, a *server.App) error {
335 proxyURL := a.Config.Build.ProxyURL
336 resp, err := http.Post(proxyURL+"/github/device/code", "application/json", nil)
337 if err != nil {
338 return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
339 }
340 defer resp.Body.Close()
341
342 if resp.StatusCode != http.StatusOK {
343 body, _ := io.ReadAll(resp.Body)
344 return c.JSON(resp.StatusCode, map[string]string{"error": string(body)})
345 }
346
347 var deviceCode map[string]interface{}
348 if err := json.NewDecoder(resp.Body).Decode(&deviceCode); err != nil {
349 return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to decode response"})
350 }
351
352 return c.JSON(http.StatusOK, deviceCode)
353}
354
355func DeviceTokenRequest(c echo.Context, a *server.App) error {
356 proxyURL := a.Config.Build.ProxyURL
357 deviceCode := c.FormValue("device_code")
358
359 resp, err := http.PostForm(proxyURL+"/github/device/token", url.Values{
360 "device_code": {deviceCode},
361 })
362 if err != nil {
363 return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
364 }
365 defer resp.Body.Close()
366
367 if resp.StatusCode != http.StatusOK {
368 body, _ := io.ReadAll(resp.Body)
369 return c.JSON(resp.StatusCode, map[string]string{"error": string(body)})
370 }
371
372 var tokenResp map[string]interface{}
373 if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
374 return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to decode response"})
375 }
376
377 return c.JSON(http.StatusOK, tokenResp)
378}