0.1.3 fb536a2

oauth.go RawHistoryFolder

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}

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