0.1.5 b849f6c

oauth.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close