Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/169/implement oauth on backend #201

Merged
merged 10 commits into from
Nov 28, 2023
77 changes: 74 additions & 3 deletions backend/app/api/endpoints/login.py
johndpjr marked this conversation as resolved.
Show resolved Hide resolved
johndpjr marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@
from dotenv import load_dotenv
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from google.auth.transport import requests
from google.oauth2 import id_token
from jose import JWTError, jwt
from pydantic import BaseModel
from sqlalchemy.orm import Session

from backend.app.core import settings
from backend.app.crud import crud
from backend.app.database import engine
from backend.app.models import User as UserModel

from ..deps import get_db

load_dotenv()
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
JWT_SECRET_KEY = settings.JWT_SECRET_KEY
ALGORITHM = settings.ALGORITHM
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES

# Google Client ID
CLIENT_ID = settings.CLIENT_ID

users_db = get_db()

Expand All @@ -39,6 +45,8 @@ def hash_password(password: str):

class User(BaseModel):
username: str
google_id: str | None = None
made_password: bool | None = None
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
Expand Down Expand Up @@ -110,6 +118,11 @@ def authenticate_user(username: str, password: str, db: Session = Depends(get_db
user = get_user(db, username)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
if not user.made_password:
raise HTTPException(
status_code=400,
detail="This account didn't make a password. Please login via Google.",
)
encoded_password = password.encode("utf-8")
if not bcrypt.checkpw(encoded_password, user.password.encode("utf-8")):
raise HTTPException(status_code=400, detail="Incorrect username or password")
Expand Down Expand Up @@ -155,6 +168,7 @@ async def register_user(
"full_name": full_name,
"email": email,
"password": hashed_password.decode("utf-8"),
"made_password": True,
"disabled": False,
}
)
Expand All @@ -176,3 +190,60 @@ async def delete_user(
return {"deleted": True}
else:
return {"deleted": False}


@router.post("/users/google-login")
async def google_login(token: str, db: Session = Depends(get_db)):
try:
# Specify the CLIENT_ID of the app that accesses the backend:
idinfo = id_token.verify_oauth2_token(
token,
requests.Request(),
CLIENT_ID,
clock_skew_in_seconds=1000000,
)
# print("ID_info:", idinfo)
# Or, if multiple clients access the backend server:
# idinfo = id_token.verify_oauth2_token(token, requests.Request())
# if idinfo['aud'] not in [CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]:
# raise ValueError('Could not verify audience.')

# If auth request is from a G Suite domain:
# if idinfo['hd'] != GSUITE_DOMAIN_NAME:
# raise ValueError('Wrong hosted domain.')

# ID token is valid. Get the user's Google Account ID from the decoded token.
# print("Getting user info")
userid = idinfo["sub"]
name = idinfo["name"]
# print(userid)
email = idinfo["email"]
# print(email)
user = crud.search_users_by_google_id(db, userid)
if user is None or len(user) == 0:
# Register a new user
new_user = UserModel(
**{
"username": "{}.{}".format(email, userid),
"google_id": userid,
"made_password": False,
"full_name": name,
"email": email,
"password": "",
"disabled": False,
}
)
user_list = [new_user]
crud.create_users(db, *user_list)
user = new_user
else:
user = user[0]
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

except ValueError as e:
# Invalid token
return {"Error": e}
4 changes: 4 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class Base(BaseSettings):
API_V1_STR = ""
HOST: str = None
PORT: int = None
JWT_SECRET_KEY: str = None
ALGORITHM: str = None
ACCESS_TOKEN_EXPIRE_MINUTES: int = None
CLIENT_ID: str = None


class Dev(Base):
Expand Down
14 changes: 14 additions & 0 deletions backend/app/crud/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ def search_users(db: Session, username: str, skip: int = 0, limit: int = 1000):
return convert_users(*results)


def search_users_by_google_id(
db: Session, field_val: str, skip: int = 0, limit: int = 1000
):
"""Searches for users by a given field value in a field"""
results = (
db.query(UserModel)
.filter(UserModel.google_id == field_val)
.offset(skip)
.limit(limit)
.all()
)
return convert_users(*results)


def delete_user(db: Session, username: str):
"""Deletes a user with the given username"""
user = db.query(UserModel).filter(UserModel.username == username).first()
Expand Down
4 changes: 3 additions & 1 deletion backend/app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy import Boolean, Column, Integer, String

from backend.app.database import DatabaseModel

Expand All @@ -7,6 +7,8 @@ class User(DatabaseModel):
__tablename__ = "user"

id = Column(Integer, primary_key=True, index=True)
google_id = Column(String)
made_password = Column(Boolean)
username = Column(String)
password = Column(String)
full_name = Column(String)
Expand Down
2 changes: 2 additions & 0 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

class UserBase(BaseModel):
id: Union[int, None] = None
google_id: str = ""
made_password: bool = False
username: str = ""
password: str = ""
full_name: str = ""
Expand Down
7 changes: 5 additions & 2 deletions backend/app/utils/logger.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import logging
import logging.handlers
from os import makedirs
import time
from os import getpid, makedirs
from pathlib import Path


def setup_logger(name):
# logger settings
makedirs(Path.cwd().joinpath("log"), exist_ok=True)
file = "log/agtern.log"
file = "log/agtern.log.{}.{}".format(
time.strftime("(%Y-%m-%d)-(%H-%M-%S)"), getpid()
)
format = "%(asctime)s [%(levelname)s] %(message)s"

# setup handlers and formatters
Expand Down
4 changes: 4 additions & 0 deletions envs/dev.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
HOST="0.0.0.0"
PORT=8000
JWT_SECRET_KEY="33d00a5e45ca905bd7276dc22dfc7459cf1c3f24d11e13f479868cca052cc225"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=30
CLIENT_ID="710734565405-3nkf5plf0m4p460osals94rnksheoh93.apps.googleusercontent.com"
22 changes: 22 additions & 0 deletions frontend/src/_generated/api/services/LoginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ export class LoginService {
});
}

/**
* GoogleLogin
* Logins a user via google
* @param id_token
* @returns any Successful Response
* @throws ApiError
*/
public static googleLogin(
token: string,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/login/users/google-login',
query: {
'token': token,
},
errors: {
422: `Validation Error`,
},
});
}

/**
* Read Users Me
* @returns any Successful Response
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/pages/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</div>

<div class="login-button">
<app-google-sign-in></app-google-sign-in>
<app-google-sign-in [loginRef]="selfRef"></app-google-sign-in>
</div>

<div class="login-button">
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/pages/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { AuthService } from '../../shared/services/auth.service';
styleUrls: ['./login.component.scss']
})
export class LoginComponent {
// Reference to itself
selfRef: LoginComponent = this;
constructor(
private authService: AuthService,
public router: Router
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Input, NgZone } from '@angular/core';
import 'zone.js';
import { LoginService } from 'src/_generated/api';
import { Router } from '@angular/router';
import { LoginComponent } from 'src/app/pages/login/login.component';

@Component({
selector: 'app-google-sign-in',
templateUrl: './google-sign-in.component.html',
styleUrls: ['./google-sign-in.component.scss']
})
export class GoogleSignInComponent implements OnInit {
constructor() {}
@Input() loginRef!: LoginComponent;
constructor(private zone: NgZone) {}

ngOnInit(): void {
// @ts-ignore
Expand All @@ -28,7 +33,18 @@ export class GoogleSignInComponent implements OnInit {
// google.accounts.id.prompt((notification: PromptMomentNotification) => {});
}

async handleCredentialResponse(response: any) {
console.log(response);
async handleCredentialResponse(googleUser: any) {
var token: string = googleUser.credential;

LoginService.googleLogin(token).then(
() => {
// login
this.loginRef.form.reset();
this.zone.run(() => this.loginRef.router.navigate(['/jobs']));
},
() => {
// Do nothing since we failed
}
);
}
}
2 changes: 2 additions & 0 deletions frontend/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@900&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
<script src="https://apis.google.com/js/platform.js" async defer></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<meta name="google-signin-client_id" content="710734565405-3nkf5plf0m4p460osals94rnksheoh93.apps.googleusercontent.com">
</head>
<body class="mat-typography mat-app-background">
<app-root></app-root>
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
bcrypt==4.0.1
celery[redis]
fastapi==0.95.2
google-api-python-client==2.108.0
nltk==3.8.1
passlib[bcrypt]==1.7.4
pre-commit==3.5.0
Expand Down