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

Refactor/205/refactor login api #210

Merged
merged 6 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading