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