abdullah090809 commited on
Commit
2d2110c
·
0 Parent(s):

Auth, Login, main , token schema user schema database.py and security.py added

Browse files
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment
2
+ .env
3
+ .env.*
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.pyc
8
+ *.pyo
9
+
10
+ # Virtual environment
11
+ venv/
12
+ env/
13
+ .venv/
14
+
15
+ # Alembic - keep versions folder but ignore pyc
16
+ alembic/versions/*.pyc
17
+
18
+ # IDE
19
+ .vscode/
20
+ .idea/
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Logs
27
+ *.log
alembic.ini ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts.
5
+ # this is typically a path given in POSIX (e.g. forward slashes)
6
+ # format, relative to the token %(here)s which refers to the location of this
7
+ # ini file
8
+ script_location = %(here)s/alembic
9
+
10
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
11
+ # Uncomment the line below if you want the files to be prepended with date and time
12
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
13
+ # for all available tokens
14
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15
+ # Or organize into date-based subdirectories (requires recursive_version_locations = true)
16
+ # file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
17
+
18
+ # sys.path path, will be prepended to sys.path if present.
19
+ # defaults to the current working directory. for multiple paths, the path separator
20
+ # is defined by "path_separator" below.
21
+ prepend_sys_path = .
22
+
23
+
24
+ # timezone to use when rendering the date within the migration file
25
+ # as well as the filename.
26
+ # If specified, requires the tzdata library which can be installed by adding
27
+ # `alembic[tz]` to the pip requirements.
28
+ # string value is passed to ZoneInfo()
29
+ # leave blank for localtime
30
+ # timezone =
31
+
32
+ # max length of characters to apply to the "slug" field
33
+ # truncate_slug_length = 40
34
+
35
+ # set to 'true' to run the environment during
36
+ # the 'revision' command, regardless of autogenerate
37
+ # revision_environment = false
38
+
39
+ # set to 'true' to allow .pyc and .pyo files without
40
+ # a source .py file to be detected as revisions in the
41
+ # versions/ directory
42
+ # sourceless = false
43
+
44
+ # version location specification; This defaults
45
+ # to <script_location>/versions. When using multiple version
46
+ # directories, initial revisions must be specified with --version-path.
47
+ # The path separator used here should be the separator specified by "path_separator"
48
+ # below.
49
+ # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
50
+
51
+ # path_separator; This indicates what character is used to split lists of file
52
+ # paths, including version_locations and prepend_sys_path within configparser
53
+ # files such as alembic.ini.
54
+ # The default rendered in new alembic.ini files is "os", which uses os.pathsep
55
+ # to provide os-dependent path splitting.
56
+ #
57
+ # Note that in order to support legacy alembic.ini files, this default does NOT
58
+ # take place if path_separator is not present in alembic.ini. If this
59
+ # option is omitted entirely, fallback logic is as follows:
60
+ #
61
+ # 1. Parsing of the version_locations option falls back to using the legacy
62
+ # "version_path_separator" key, which if absent then falls back to the legacy
63
+ # behavior of splitting on spaces and/or commas.
64
+ # 2. Parsing of the prepend_sys_path option falls back to the legacy
65
+ # behavior of splitting on spaces, commas, or colons.
66
+ #
67
+ # Valid values for path_separator are:
68
+ #
69
+ # path_separator = :
70
+ # path_separator = ;
71
+ # path_separator = space
72
+ # path_separator = newline
73
+ #
74
+ # Use os.pathsep. Default configuration used for new projects.
75
+ path_separator = os
76
+
77
+ # set to 'true' to search source files recursively
78
+ # in each "version_locations" directory
79
+ # new in Alembic version 1.10
80
+ # recursive_version_locations = false
81
+
82
+ # the output encoding used when revision files
83
+ # are written from script.py.mako
84
+ # output_encoding = utf-8
85
+
86
+ # database URL. This is consumed by the user-maintained env.py script only.
87
+ # other means of configuring database URLs may be customized within the env.py
88
+ # file.
89
+ sqlalchemy.url = driver://user:pass@localhost/dbname
90
+
91
+
92
+ [post_write_hooks]
93
+ # post_write_hooks defines scripts or Python functions that are run
94
+ # on newly generated revision scripts. See the documentation for further
95
+ # detail and examples
96
+
97
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
98
+ # hooks = black
99
+ # black.type = console_scripts
100
+ # black.entrypoint = black
101
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
102
+
103
+ # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
104
+ # hooks = ruff
105
+ # ruff.type = module
106
+ # ruff.module = ruff
107
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
108
+
109
+ # Alternatively, use the exec runner to execute a binary found on your PATH
110
+ # hooks = ruff
111
+ # ruff.type = exec
112
+ # ruff.executable = ruff
113
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
114
+
115
+ # Logging configuration. This is also consumed by the user-maintained
116
+ # env.py script only.
117
+ [loggers]
118
+ keys = root,sqlalchemy,alembic
119
+
120
+ [handlers]
121
+ keys = console
122
+
123
+ [formatters]
124
+ keys = generic
125
+
126
+ [logger_root]
127
+ level = WARNING
128
+ handlers = console
129
+ qualname =
130
+
131
+ [logger_sqlalchemy]
132
+ level = WARNING
133
+ handlers =
134
+ qualname = sqlalchemy.engine
135
+
136
+ [logger_alembic]
137
+ level = INFO
138
+ handlers =
139
+ qualname = alembic
140
+
141
+ [handler_console]
142
+ class = StreamHandler
143
+ args = (sys.stderr,)
144
+ level = NOTSET
145
+ formatter = generic
146
+
147
+ [formatter_generic]
148
+ format = %(levelname)-5.5s [%(name)s] %(message)s
149
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/env.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.cores.database import Base
2
+ from logging.config import fileConfig
3
+ from sqlalchemy import engine_from_config
4
+ from sqlalchemy import pool
5
+ from alembic import context
6
+ from app.cores.config import settings
7
+ from app.models.user import User
8
+ config = context.config
9
+
10
+ config.set_main_option("sqlalchemy.url", f"postgresql://{settings.DATABASE_USERNAME}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOSTNAME}:{settings.DATABASE_PORT}/{settings.DATABASE_NAME}")
11
+
12
+ if config.config_file_name is not None:
13
+ fileConfig(config.config_file_name)
14
+
15
+ target_metadata = Base.metadata
16
+
17
+
18
+ def run_migrations_offline() -> None:
19
+ url = config.get_main_option("sqlalchemy.url")
20
+ context.configure(
21
+ url=url,
22
+ target_metadata=target_metadata,
23
+ literal_binds=True,
24
+ dialect_opts={"paramstyle": "named"},
25
+ )
26
+
27
+ with context.begin_transaction():
28
+ context.run_migrations()
29
+
30
+
31
+ def run_migrations_online() -> None:
32
+ connectable = engine_from_config(
33
+ config.get_section(config.config_ini_section, {}),
34
+ prefix="sqlalchemy.",
35
+ poolclass=pool.NullPool,
36
+ )
37
+
38
+ with connectable.connect() as connection:
39
+ context.configure(
40
+ connection=connection, target_metadata=target_metadata
41
+ )
42
+
43
+ with context.begin_transaction():
44
+ context.run_migrations()
45
+
46
+
47
+ if context.is_offline_mode():
48
+ run_migrations_offline()
49
+ else:
50
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
alembic/versions/6f056783c0d8_create_users_table.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """create users table
2
+
3
+ Revision ID: 6f056783c0d8
4
+ Revises:
5
+ Create Date: 2026-06-29 16:16:28.748674
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '6f056783c0d8'
16
+ down_revision: Union[str, Sequence[str], None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('users',
25
+ sa.Column('id', sa.Integer(), nullable=False),
26
+ sa.Column('email', sa.String(), nullable=False),
27
+ sa.Column('hashed_password', sa.String(), nullable=False),
28
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
29
+ sa.PrimaryKeyConstraint('id'),
30
+ sa.UniqueConstraint('email')
31
+ )
32
+ # ### end Alembic commands ###
33
+
34
+
35
+ def downgrade() -> None:
36
+ """Downgrade schema."""
37
+ # ### commands auto generated by Alembic - please adjust! ###
38
+ op.drop_table('users')
39
+ # ### end Alembic commands ###
app/__init__.py ADDED
File without changes
app/cores/__init__.py ADDED
File without changes
app/cores/config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from pathlib import Path
3
+
4
+ env_path = Path(__file__).resolve().parent.parent.parent / ".env"
5
+
6
+ class Settings(BaseSettings):
7
+ DATABASE_HOSTNAME: str
8
+ DATABASE_PORT: str
9
+ DATABASE_PASSWORD: str
10
+ DATABASE_NAME: str
11
+ DATABASE_USERNAME: str
12
+ SECRET_KEY: str
13
+ ALGORITHM: str
14
+ ACCESS_TOKEN_EXPIRE_MINUTES: int
15
+ GROQ_API_KEY: str
16
+
17
+ model_config = {"env_file": str(env_path), "env_file_encoding": "utf-8"}
18
+
19
+ settings = Settings()
app/cores/database.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import final
2
+
3
+ from sqlalchemy import create_engine
4
+ from sqlalchemy.orm import declarative_base, sessionmaker
5
+
6
+ from app.cores.config import settings
7
+
8
+ SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.DATABASE_USERNAME}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOSTNAME}:{settings.DATABASE_PORT}/{settings.DATABASE_NAME}"
9
+
10
+ engine = create_engine(SQLALCHEMY_DATABASE_URL)
11
+ SessionLocal = sessionmaker(autocommit=False,autoflush=False,blind=engine)
12
+ Base = declarative_base()
13
+
14
+ def get_db():
15
+ db = SessionLocal()
16
+ try:
17
+ yield db
18
+ finally:
19
+ db.close()
app/cores/security.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from fastapi.security import OAuth2PasswordBearer
3
+ from jose import jwt
4
+ from app.cores.config import settings
5
+ from passlib.context import CryptContext
6
+ from fastapi import Depends, HTTPException, status
7
+ from app.cores.database import get_db
8
+ from app.models.user import User
9
+ from app.schemas.token import TokenData
10
+ from sqlalchemy.orm import Session
11
+
12
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
13
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
14
+
15
+ def hash_password(password: str) -> str:
16
+ return pwd_context.hash(password)
17
+
18
+ def verify_password(plain_password: str, hashed_password: str) -> str:
19
+ return pwd_context.verify(plain_password,hashed_password)
20
+
21
+ def create_access_token(data: dict) -> str:
22
+ to_encode = data.copy()
23
+ expire = datetime.utcnow() + datetime.timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
24
+ to_encode.update["exp": expire]
25
+ return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
26
+
27
+ def decode_token(token: str) -> dict:
28
+ return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
29
+
30
+ def get_current_user(token: str = Depends(oauth2_scheme), db: Session= Depends(get_db())):
31
+ credentials_exception = HTTPException(
32
+ status_code=status.HTTP_401_UNAUTHORIZED,
33
+ detail="Could not validate credentials",
34
+ headers={"WWW-Authenticate": "Bearer"},
35
+ )
36
+ try:
37
+ payload = decode_token(token)
38
+ user_id: int = TokenData(payload.get("user_id"))
39
+ if user_id is None:
40
+ raise credentials_exception
41
+ except:
42
+ raise credentials_exception
43
+ user = db.query(User).filter(user_id == User.id).first()
44
+ if not user:
45
+ raise credentials_exception
46
+ return user
47
+
48
+
app/main.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from app.routers import auth, user
3
+
4
+ app = FastAPI(title="Job Tracker API")
5
+
6
+ app.include_router(auth.router)
7
+ app.include_router(user.router)
8
+ @app.get("/")
9
+ def root():
10
+ return {"message": "Job Tracker API is running"}
app/models/__init__.py ADDED
File without changes
app/models/user.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, DateTime, Integer, String
2
+ from sqlalchemy.sql import func
3
+ from sqlalchemy.orm import relationship
4
+ from app.cores.database import Base
5
+
6
+ class User(Base):
7
+ __tablename__ = "users"
8
+
9
+ id = Column(Integer, primary_key=True, nullable=False)
10
+ email = Column(String, unique=True, nullable=False)
11
+ hashed_password = Column(String, nullable=False)
12
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
13
+
14
+ application = relationship("Applications", back_populates="owner")
app/routers/__init__.py ADDED
File without changes
app/routers/auth.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from fastapi.security import OAuth2PasswordRequestForm
3
+ from app.cores.security import create_access_token, verify_password
4
+ from app.models.user import User
5
+ from app.schemas.token import Token
6
+ from app.cores.database import get_db
7
+ from sqlalchemy.orm import Session
8
+
9
+
10
+ router = APIRouter(tags=["Authentication"])
11
+
12
+ router.post("/login", response_model=Token)
13
+ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db())):
14
+ user = db.query(User).filter(User.email == form_data.username).first()
15
+ if not user or not verify_password(form_data.password, user.hashed_password):
16
+ raise HTTPException(status_code=401, detail="Invalid credentials")
17
+ token = create_access_token({"user_id": user.id})
18
+ return {
19
+ "access_token": token,
20
+ "token_type": "bearer"
21
+ }
22
+
23
+
24
+
25
+
app/routers/user.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends
2
+ from sqlalchemy.orm import Session
3
+ from app.cores.security import hash_password
4
+ from app.models.user import User
5
+ from app.schemas.user import UserCreate, UserOut
6
+ from app.cores.database import get_db
7
+
8
+ router = APIRouter(
9
+ prefix="/users",
10
+ tags=["login"]
11
+ )
12
+
13
+
14
+ @router.post("/", response_model=UserOut, status_code=status.HTTP_201_CREATED)
15
+ def create_user(user: UserCreate, db: Session = Depends(get_db)):
16
+ existing = db.query(User).filter(User.email == user.email).first()
17
+
18
+ if existing:
19
+ raise HTTPException(
20
+ status_code=status.HTTP_400_BAD_REQUEST,
21
+ detail="Email already registered"
22
+ )
23
+
24
+ new_user = User(
25
+ email=user.email,
26
+ hashed_password=hash_password(user.password)
27
+ )
28
+
29
+ db.add(new_user)
30
+ db.commit()
31
+ db.refresh(new_user)
32
+
33
+ return new_user
app/schemas/__init__.py ADDED
File without changes
app/schemas/token.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class Token(BaseModel):
4
+ access_token: str
5
+ token_type: str
6
+
7
+ class TokenData(BaseModel):
8
+ id: int
app/schemas/user.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from pydantic import BaseModel, EmailStr
3
+
4
+
5
+ class UserCreate(BaseModel):
6
+ email: EmailStr
7
+ password: str
8
+
9
+ class UserOut(BaseModel):
10
+ id: int
11
+ email: EmailStr
12
+ created_at: datetime
13
+ model_config = {
14
+ "from_attribute": True
15
+ }
requirements.txt ADDED
Binary file (2.21 kB). View file