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