Skip to content

Commit

Permalink
Merge pull request #72 from Bikram-ghuku/main
Browse files Browse the repository at this point in the history
Added github OAuth endpoints
  • Loading branch information
rajivharlalka authored Jul 21, 2024
2 parents 3e5dbd5 + d5532e9 commit d011ba2
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 6 deletions.
7 changes: 6 additions & 1 deletion backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ DB_USER=
DB_PASSWORD=
STATIC_FILES_STORAGE_LOCATION=/srv/static
UPLOADED_QPS_PATH=iqps/uploaded # Relative to `STATIC_FILES_STORAGE_LOCATION`. Final upload location will be /srv/static/iqps/uploaded
MAX_UPLOAD_LIMIT=10
MAX_UPLOAD_LIMIT=10
GH_CLIENT_ID= # public token of the oauth app
GH_PRIVATE_ID= # Private token of the oauth app
JWT_SECRET= # JWT encryption secret
GH_ORG_NAME= # name of the org
GH_ORG_TEAM_SLUG= #URL friendly team Name
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21 AS builder
FROM golang:1.22.4 AS builder

WORKDIR /src

Expand Down
4 changes: 3 additions & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
module github.com/metakgp/iqps/backend

go 1.21.6
go 1.22.4

require github.com/rs/cors v1.10.1

require github.com/lib/pq v1.10.9

require github.com/golang-jwt/jwt/v5 v5.2.1
239 changes: 238 additions & 1 deletion backend/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"database/sql"
"encoding/json"
"errors"
Expand All @@ -17,8 +18,13 @@ import (
"github.com/rs/cors"

_ "github.com/lib/pq"
"github.com/rs/cors"
)

type contextKey string

const claimsKey = contextKey("claims")

type QuestionPaper struct {
ID int `json:"id"`
CourseCode string `json:"course_code"`
Expand All @@ -43,8 +49,32 @@ var (
staticFilesUrl string
staticFilesStorageLocation string
uploadedQpsPath string
gh_pubKey string
gh_pvtKey string
jwt_secret string
org_name string
org_team string
)

type GhOAuthReqBody struct {
GhCode string `json:"code"`
}

type GithubAccessTokenResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}

type GithubUserResponse struct {
Login string `json:"login"`
ID int `json:"id"`
}

var respData struct {
Token string `json:"token"`
}

const init_db = `CREATE TABLE IF NOT EXISTS qp (
id SERIAL PRIMARY KEY,
course_code TEXT NOT NULL DEFAULT '',
Expand Down Expand Up @@ -308,17 +338,222 @@ func populateDB(filename string) error {
return nil
}

func GhAuth(w http.ResponseWriter, r *http.Request) {

ghOAuthReqBody := GhOAuthReqBody{}
if err := json.NewDecoder(r.Body).Decode(&ghOAuthReqBody); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if ghOAuthReqBody.GhCode == "" {
http.Error(w, "Github OAuth Code cannot be empty", http.StatusBadRequest)
return
}

// Get the access token for authenticating other endpoints
uri := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", gh_pubKey, gh_pvtKey, ghOAuthReqBody.GhCode)

req, _ := http.NewRequest("POST", uri, nil)
req.Header.Set("Accept", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error Getting Github Access Token: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// Decode the response
var tokenResponse GithubAccessTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
fmt.Println("Error Decoding Github Access Token: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

// Get the username of the user who made the request
req, _ = http.NewRequest("GET", "https://api.github.com/user", nil)
req.Header.Set("Authorization", "Bearer "+tokenResponse.AccessToken)

resp, err = client.Do(req)
if err != nil {
fmt.Println("Error getting username: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()

// Decode the response
var userResponse GithubUserResponse
if err := json.NewDecoder(resp.Body).Decode(&userResponse); err != nil {
fmt.Println("Error decoding username: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

uname := userResponse.Login
// check if uname is empty
if uname == "" {
http.Error(w, "No user found", http.StatusUnauthorized)
return
}

// Send request to check status of the user in the given org's team
url := fmt.Sprintf("https://api.github.com/orgs/%s/teams/%s/memberships/%s", org_name, org_team, uname)
req, _ = http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+tokenResponse.AccessToken)
resp, err = client.Do(req)

var checkResp struct {
State string `json:"state"`
}

if err != nil {
fmt.Println("Error validating user membership: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

defer resp.Body.Close()
//decode the response
if err := json.NewDecoder(resp.Body).Decode(&checkResp); err != nil {
fmt.Println("Error decoding gh validation body: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

// Check if user is present in the team
if checkResp.State != "active" {

http.Error(w, "User is not authenticated", http.StatusUnauthorized)
return
}

// Create the response JWT
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": uname,
})

tokenString, err := token.SignedString([]byte(jwt_secret))
if err != nil {
fmt.Println("Error Sigining JWT: ", err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

http.Header.Add(w.Header(), "content-type", "application/json")

// Send the response

respData.Token = tokenString
err = json.NewEncoder(w).Encode(&respData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

func JWTMiddleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// get the authorisation header
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "Missing authorization header")
return
}
JWTtoken := strings.Split(tokenString, " ")

if len(JWTtoken) != 2 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "Authorisation head is of incorrect type")
return
}
//parse the token
token, err := jwt.Parse(JWTtoken[1], func(t *jwt.Token) (interface{}, error) {
if _, OK := t.Method.(*jwt.SigningMethodHMAC); !OK {
return nil, errors.New("bad signed method received")
}

return []byte(jwt_secret), nil
})

// Check if error in parsing jwt token
if err != nil {
http.Error(w, "Bad JWT token", http.StatusUnauthorized)
return
}
// Get the claims
claims, ok := token.Claims.(jwt.MapClaims)

if ok && token.Valid && claims["username"] != nil {
// If valid claims found, send response
ctx := context.WithValue(r.Context(), claimsKey, claims)
handler.ServeHTTP(w, r.WithContext(ctx))
} else {
http.Error(w, "Invalid JWT token", http.StatusUnauthorized)
}
})
}

func getClaims(r *http.Request) jwt.MapClaims {
if claims, ok := r.Context().Value(claimsKey).(jwt.MapClaims); ok {
return claims
}
return nil
}

// func protectedRoute(w http.ResponseWriter, r *http.Request) {
// claims := getClaims(r)

// if claims != nil {
// fmt.Fprintf(w, "Hello, %s", claims["username"])
// } else {
// http.Error(w, "No claims found", http.StatusUnauthorized)
// }
// }

func CheckError(err error) {
if err != nil {
panic(err)
}
}

func LoadGhEnv() {
gh_pubKey = os.Getenv("GH_CLIENT_ID")
gh_pvtKey = os.Getenv("GH_PRIVATE_ID")
org_name = os.Getenv("GH_ORG_NAME")
org_team = os.Getenv("GH_ORG_TEAM_SLUG")

jwt_secret = os.Getenv("JWT_SECRET")

if gh_pubKey == "" {
panic("Client id for Github OAuth cannot be empty")
}
if gh_pvtKey == "" {
panic("Client Private Key for Github OAuth cannot be empty")
}
if org_name == "" {
panic("Organisation name cannot be empty")
}
if org_team == "" {
panic("Team name of the Organistion cannot be empty")
}
if jwt_secret == "" {
panic("JWT Secret Key cannot be empty")
}
}

func main() {
host := os.Getenv("DB_HOST")
port, err := strconv.Atoi(os.Getenv("DB_PORT"))
CheckError(err)

LoadGhEnv()

user := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
dbname := os.Getenv("DB_NAME")
Expand All @@ -345,7 +580,9 @@ func main() {
http.HandleFunc("/search", search)
http.HandleFunc("/year", year)
http.HandleFunc("/library", library)
http.HandleFunc("/upload", upload)
http.HandleFunc("POST /upload", upload)
http.HandleFunc("GET /oauth", GhAuth)
//http.Handle("/protected", JWTMiddleware(http.HandlerFunc(protectedRoute)))

c := cors.New(cors.Options{
AllowedOrigins: []string{"https://qp.metakgp.org", "http://localhost:3000"},
Expand Down
2 changes: 1 addition & 1 deletion crawler/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/metakgp/iqps/crawler

go 1.21.6
go 1.22.4

require github.com/gocolly/colly v1.2.0

Expand Down
2 changes: 1 addition & 1 deletion go.work
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
go 1.21.6
go 1.22.4

use (
./backend
Expand Down

0 comments on commit d011ba2

Please sign in to comment.