Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5de2b4c

Browse filesBrowse files
committed
Merge branch 'python_fastapi_crud_api'
2 parents 2e6ef33 + ed9c5e1 commit 5de2b4c
Copy full SHA for 5de2b4c

File tree

6 files changed

+119
-43
lines changed
Filter options

6 files changed

+119
-43
lines changed
+43Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""added verification code
2+
3+
Revision ID: 39256113e8e5
4+
Revises: 1c7984990e1d
5+
Create Date: 2022-07-14 08:03:57.507140
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '39256113e8e5'
14+
down_revision = '1c7984990e1d'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.drop_table('posts')
22+
op.add_column('users', sa.Column('verification_code', sa.String(), nullable=True))
23+
op.create_unique_constraint(None, 'users', ['verification_code'])
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade() -> None:
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
op.drop_constraint(None, 'users', type_='unique')
30+
op.drop_column('users', 'verification_code')
31+
op.create_table('posts',
32+
sa.Column('id', postgresql.UUID(), server_default=sa.text('uuid_generate_v4()'), autoincrement=False, nullable=False),
33+
sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=False),
34+
sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=False),
35+
sa.Column('content', sa.VARCHAR(), autoincrement=False, nullable=False),
36+
sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=False),
37+
sa.Column('image', sa.VARCHAR(), autoincrement=False, nullable=False),
38+
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
39+
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
40+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='posts_user_id_fkey', ondelete='CASCADE'),
41+
sa.PrimaryKeyConstraint('id', name='posts_pkey')
42+
)
43+
# ### end Alembic commands ###
+39Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""added post table
2+
3+
Revision ID: 4917da928a79
4+
Revises: 39256113e8e5
5+
Create Date: 2022-07-14 09:05:17.444518
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '4917da928a79'
14+
down_revision = '39256113e8e5'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table('posts',
22+
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
23+
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
24+
sa.Column('title', sa.String(), nullable=False),
25+
sa.Column('content', sa.String(), nullable=False),
26+
sa.Column('category', sa.String(), nullable=False),
27+
sa.Column('image', sa.String(), nullable=False),
28+
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
29+
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
30+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
31+
sa.PrimaryKeyConstraint('id')
32+
)
33+
# ### end Alembic commands ###
34+
35+
36+
def downgrade() -> None:
37+
# ### commands auto generated by Alembic - please adjust! ###
38+
op.drop_table('posts')
39+
# ### end Alembic commands ###

‎app/models.py

Copy file name to clipboardExpand all lines: app/models.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class User(Base):
1414
password = Column(String, nullable=False)
1515
photo = Column(String, nullable=True)
1616
verified = Column(Boolean, nullable=False, server_default='False')
17+
verification_code = Column(String, nullable=True, unique=True)
1718
role = Column(String, server_default='user', nullable=False)
1819
created_at = Column(TIMESTAMP(timezone=True),
1920
nullable=False, server_default=text("now()"))

‎app/oauth2.py

Copy file name to clipboardExpand all lines: app/oauth2.py
-27Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import base64
2-
from datetime import datetime, timedelta
32
from typing import List
43
from fastapi import Depends, HTTPException, status
54
from fastapi_jwt_auth import AuthJWT
65
from pydantic import BaseModel
7-
from jose import jwt, JWTError
86

97
from . import models
108
from .database import get_db
@@ -65,28 +63,3 @@ def require_user(db: Session = Depends(get_db), Authorize: AuthJWT = Depends()):
6563
raise HTTPException(
6664
status_code=status.HTTP_401_UNAUTHORIZED, detail='Token is invalid or has expired')
6765
return user_id
68-
69-
70-
def create_verification_token(user_id: str):
71-
expires = datetime.utcnow() + timedelta(minutes=30)
72-
payload = {'user_id': user_id, 'exp': expires}
73-
return jwt.encode(payload, settings.VERIFICATION_SECRET, algorithm='HS256')
74-
75-
76-
def verify_email_token(token: str):
77-
try:
78-
decoded = jwt.decode(
79-
token, settings.VERIFICATION_SECRET, algorithms=['HS256'])
80-
user_id = decoded.get('user_id')
81-
if not user_id:
82-
raise HTTPException(
83-
status_code=status.HTTP_400_BAD_REQUEST, detail='Could not verify user')
84-
except JWTError as e:
85-
error = e.__class__.__name__
86-
print(error)
87-
if error == 'ExpiredSignatureError':
88-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
89-
detail='Token is invalid or has expired')
90-
raise HTTPException(
91-
status_code=status.HTTP_400_BAD_REQUEST, detail='Could not verify user')
92-
return user_id

‎app/routers/auth.py

Copy file name to clipboardExpand all lines: app/routers/auth.py
+26-8Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from datetime import timedelta
2+
import hashlib
3+
from random import randbytes
24
from fastapi import APIRouter, Request, Response, status, Depends, HTTPException
35
from pydantic import EmailStr
46

@@ -19,8 +21,9 @@
1921
@router.post('/register', status_code=status.HTTP_201_CREATED)
2022
async def create_user(payload: schemas.CreateUserSchema, request: Request, db: Session = Depends(get_db)):
2123
# Check if user already exist
22-
user = db.query(models.User).filter(
23-
models.User.email == EmailStr(payload.email.lower())).first()
24+
user_query = db.query(models.User).filter(
25+
models.User.email == EmailStr(payload.email.lower()))
26+
user = user_query.first()
2427
if user:
2528
raise HTTPException(status_code=status.HTTP_409_CONFLICT,
2629
detail='Account already exist')
@@ -40,11 +43,21 @@ async def create_user(payload: schemas.CreateUserSchema, request: Request, db: S
4043
db.refresh(new_user)
4144

4245
try:
43-
token = oauth2.create_verification_token(str(new_user.id))
44-
url = f"{request.url.scheme}://{request.client.host}:{request.url.port}/api/auth/verifyemail/{token}"
46+
# Send Verification Email
47+
token = randbytes(10)
48+
hashedCode = hashlib.sha256()
49+
hashedCode.update(token)
50+
verification_code = hashedCode.hexdigest()
51+
user_query.update(
52+
{'verification_code': verification_code}, synchronize_session=False)
53+
db.commit()
54+
url = f"{request.url.scheme}://{request.client.host}:{request.url.port}/api/auth/verifyemail/{token.hex()}"
4555
await Email(new_user, url, [payload.email]).sendVerificationCode()
4656
except Exception as error:
4757
print('Error', error)
58+
user_query.update(
59+
{'verification_code': None}, synchronize_session=False)
60+
db.commit()
4861
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
4962
detail='There was an error sending email')
5063
return {'status': 'success', 'message': 'Verification token successfully sent to your email'}
@@ -129,13 +142,18 @@ def logout(response: Response, Authorize: AuthJWT = Depends(), user_id: str = De
129142

130143
@router.get('/verifyemail/{token}')
131144
def verify_me(token: str, db: Session = Depends(get_db)):
132-
id = oauth2.verify_email_token(token)
133-
user_query = db.query(models.User).filter(models.User.id == id)
145+
hashedCode = hashlib.sha256()
146+
hashedCode.update(bytes.fromhex(token))
147+
verification_code = hashedCode.hexdigest()
148+
user_query = db.query(models.User).filter(
149+
models.User.verification_code == verification_code)
150+
db.commit()
134151
user = user_query.first()
135152
if not user:
136153
raise HTTPException(
137-
status_code=status.HTTP_404_NOT_FOUND, detail='User no longer exist')
138-
user_query.update({'verified': True}, synchronize_session=False)
154+
status_code=status.HTTP_403_FORBIDDEN, detail='Email can only be verified once')
155+
user_query.update(
156+
{'verified': True, 'verification_code': None}, synchronize_session=False)
139157
db.commit()
140158
return {
141159
"status": "success",

‎app/schemas.py

Copy file name to clipboardExpand all lines: app/schemas.py
+10-8Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class UserResponse(UserBaseSchema):
3232

3333

3434
class FilteredUserResponse(UserBaseSchema):
35-
pass
35+
id: uuid.UUID
3636

3737

3838
class PostBaseSchema(BaseModel):
@@ -49,23 +49,25 @@ class Config:
4949
class CreatePostSchema(PostBaseSchema):
5050
pass
5151

52+
53+
class PostResponse(PostBaseSchema):
54+
id: uuid.UUID
55+
user: FilteredUserResponse
56+
created_at: datetime
57+
updated_at: datetime
58+
59+
5260
class UpdatePostSchema(BaseModel):
5361
title: str | None = None
5462
content: str | None = None
5563
category: str | None = None
5664
image: str | None = None
65+
user_id: uuid.UUID | None = None
5766

5867
class Config:
5968
orm_mode = True
6069

6170

62-
class PostResponse(PostBaseSchema):
63-
id: uuid.UUID
64-
user: FilteredUserResponse
65-
created_at: datetime
66-
updated_at: datetime
67-
68-
6971
class ListPostResponse(BaseModel):
7072
status: str
7173
results: int

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.