Skip to content

Commit

Permalink
Refactor/205/refactor login api (#210)
Browse files Browse the repository at this point in the history
* Added google sign up button by forcing sign up component to extend login component

* Fixed precommit checks

* Added delay so google sign in button shows up when switching to login and sign up and vice versa

* Fixed precommit issues

* Refactored login and separated router stuff to login.py and non router stuff to login_base.py

* AT-205: run-precommit

---------

Co-authored-by: Jwdegames <[email protected]>
  • Loading branch information
johndpjr and Jwdegames authored Sep 12, 2024
1 parent 01cc1dc commit 532f3ba
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 125 deletions.
119 changes: 10 additions & 109 deletions backend/app/api/endpoints/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,122 +12,23 @@
from pydantic import BaseModel
from sqlalchemy.orm import Session

from backend.app.api.login_base import (
ACCESS_TOKEN_EXPIRE_MINUTES,
CLIENT_ID,
User,
authenticate_user,
create_access_token,
get_current_active_user,
hashing_function,
router,
)
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 = 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()

router = APIRouter()


def hash_password(password: str):
bytes_ = password.encode("utf-8")
salt = bcrypt.gensalt()
hash_ = bcrypt.hashpw(bytes_, salt)
return hash_


hashing_function = hash_password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token")


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


class UserInDB(User):
hashed_password: str


class Token(BaseModel):
access_token: str
token_type: str


class TokenData(BaseModel):
username: str | None = None


def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


def get_user(db, username: str):
users = crud.search_users(db, username)
if len(users) > 0:
return users[0]
return None


async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Session = Depends(get_db),
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user


async def get_current_active_user(
current_user: Annotated[UserModel, Depends(get_current_user)]
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user


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")
return user


@router.post("/token")
async def login(
Expand Down
128 changes: 128 additions & 0 deletions backend/app/api/login_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os
from datetime import datetime, timedelta
from typing import Annotated

import bcrypt
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.api.deps import get_db
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

load_dotenv()
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()

router = APIRouter()


def hash_password(password: str):
bytes_ = password.encode("utf-8")
salt = bcrypt.gensalt()
hash_ = bcrypt.hashpw(bytes_, salt)
return hash_


hashing_function = hash_password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login/token")


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


class UserInDB(User):
hashed_password: str


class Token(BaseModel):
access_token: str
token_type: str


class TokenData(BaseModel):
username: str | None = None


def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


def get_user(db, username: str):
users = crud.search_users(db, username)
if len(users) > 0:
return users[0]
return None


async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Session = Depends(get_db),
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user


async def get_current_active_user(
current_user: Annotated[UserModel, Depends(get_current_user)]
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user


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")
return user
8 changes: 6 additions & 2 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ const routes: Routes = [
import('./core/main/main.module').then((m) => m.MainModule),
title: 'AgTern'
},
{ path: '**', component: NotFoundComponent, title: 'AgTern | 404 Not Found' }
{
path: '**',
component: NotFoundComponent,
title: 'AgTern | 404 Not Found Error Helpa'
}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule {}
6 changes: 6 additions & 0 deletions frontend/src/app/core/main/main-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { MainComponent } from './main.component';
import { LoginComponent } from '../../pages/login/login.component';
import { SignUpComponent } from '../../pages/sign-up/sign-up.component';
import { NotFoundComponent } from '../../pages/not-found/not-found.component';

const routes: Routes = [
{
Expand All @@ -22,6 +23,11 @@ const routes: Routes = [
title: 'AgTern | Sign Up'
}
]
},
{
path: '**',
component: NotFoundComponent,
title: 'AgTern | 404 Not Found Error Helpa'
}
];

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/pages/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { AuthService } from '../../shared/services/auth.service';
})
export class LoginComponent {
// Reference to itself
selfRef: LoginComponent = this;
public selfRef: LoginComponent = this;
constructor(
private authService: AuthService,
public authService: AuthService,
public router: Router
) {}

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/pages/sign-up/sign-up.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
<button type="submit" mat-button>Sign Up</button>
</div>

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

<div class="invalid-username" *ngIf="isTakenUsername"><p>Username is already taken</p></div>

</form>
Expand Down
12 changes: 4 additions & 8 deletions frontend/src/app/pages/sign-up/sign-up.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../../shared/services/auth.service';
import { LoginComponent } from '../login/login.component';

@Component({
selector: 'app-sign-up',
templateUrl: './sign-up.component.html',
styleUrls: ['./sign-up.component.scss']
})
export class SignUpComponent {
constructor(
public router: Router,
private authService: AuthService
) {}

export class SignUpComponent extends LoginComponent {
isTakenUsername: boolean = false;

form: FormGroup = new FormGroup({
override form: FormGroup = new FormGroup({
username: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [
Expand All @@ -25,7 +21,7 @@ export class SignUpComponent {
])
});

submit() {
override submit() {
if (this.form.valid) {
this.authService
.signUp(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'zone.js';
import { LoginService } from 'src/_generated/api';
import { Router } from '@angular/router';
import { LoginComponent } from 'src/app/pages/login/login.component';
import { SignUpComponent } from 'src/app/pages/sign-up/sign-up.component';

@Component({
selector: 'app-google-sign-in',
Expand All @@ -22,12 +23,17 @@ export class GoogleSignInComponent implements OnInit {
auto_select: false,
cancel_on_tap_outside: true
});
var sign_up_button_id = 'google-button';
// @ts-ignore
google.accounts.id.renderButton(
setTimeout(function () {
// @ts-ignore
document.getElementById('google-button'),
{ theme: 'outline', size: 'large', width: '100%' }
);
google.accounts.id.renderButton(
// @ts-ignore
document.getElementById(sign_up_button_id),
{ theme: 'outline', size: 'large', width: '100%' }
);
console.log('Delayed');
}, 100);
// @ts-ignore
// Enable for an additional "Sign-in with Google" notification on the top right
// google.accounts.id.prompt((notification: PromptMomentNotification) => {});
Expand Down

0 comments on commit 532f3ba

Please sign in to comment.