0.1.21 fbfa93c

oauth.go RawHistoryFolder

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}

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