diff --git a/.github/workflows/build-backend.yml b/.github/workflows/build-backend.yml new file mode 100644 index 0000000..bd688af --- /dev/null +++ b/.github/workflows/build-backend.yml @@ -0,0 +1,33 @@ +name: Build Frontend +on: + push: + paths: + - 'backend/**' + - '.github/workflows/**' + branches: + - main + + pull_request: + paths: + - 'backend/**' + - '.github/workflows/**' + types: + - opened + - synchronize + - reopened + +jobs: + sonarcloud: + name: SonarCloud Backend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + with: + projectBaseDir: backend + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }} diff --git a/.github/workflows/build-frontend.yml b/.github/workflows/build-frontend.yml new file mode 100644 index 0000000..f385c48 --- /dev/null +++ b/.github/workflows/build-frontend.yml @@ -0,0 +1,33 @@ +name: Build Frontend +on: + push: + paths: + - 'frontend/**' + - '.github/workflows/**' + branches: + - main + + pull_request: + paths: + - 'frontend/**' + - '.github/workflows/**' + types: + - opened + - synchronize + - reopened + +jobs: + sonarcloud: + name: SonarCloud Frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + with: + projectBaseDir: frontend + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_FRONTEND }} diff --git a/backend/app/dependencies/qrvalidation.py b/backend/app/dependencies/qrvalidation.py new file mode 100644 index 0000000..2ce85fd --- /dev/null +++ b/backend/app/dependencies/qrvalidation.py @@ -0,0 +1,62 @@ +import os +from hashlib import sha256 +import hmac +import time +import base64 + + +validity = 3600 # seconds (int) + +k = os.urandom(32) + + +def sign(data: str) -> str: + hmac_sha256 = hmac.new(k, digestmod=sha256) + hmac_sha256.update(data.encode()) + t = int(time.time()) + expiration = t + validity + expiration = expiration.to_bytes(4, byteorder='big') + hmac_sha256.update(expiration) + hash = hmac_sha256.digest() + # xor the first 16 bytes with the last 16 bytes + hash = bytes([a ^ b for a, b in zip(hash[:16], hash[16:])]) + # base85 encode the hash + signature = base64.b85encode(expiration + hash) + return signature.decode() + + +def verify(data: str) -> bool: + if len(data) < 25: + return False + + signature = base64.b85decode(data[-25:].encode()) + expiration = int.from_bytes(signature[:4], byteorder='big') + if expiration < time.time(): + return False + + msg = data[:-25] + hmac_sha256 = hmac.new(k, digestmod=sha256) + hmac_sha256.update(msg.encode()) + hmac_sha256.update(signature[:4]) + hash = hmac_sha256.digest() + hash = bytes([a ^ b for a, b in zip(hash[:16], hash[16:])]) + return hash == signature[4:] + + +def encode(data: str) -> str: + return data + sign(data) + + +def decode(data: str) -> str: + if verify(data): + return data[:-25] + return None + + +if __name__ == "__main__": + signed_msg = sign("Hello World") + print(signed_msg) + print(verify(signed_msg)) + signed_msg = signed_msg[:-1] + "A" # tamper with the message + print(verify(signed_msg)) + \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index 5adbba8..1866a7d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,8 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.dependencies.database import Base, engine - -from app.routes import auth, desafios +from app.routes import auth, qrcode, desafios from app import models Base.metadata.create_all(bind=engine) @@ -12,6 +11,7 @@ app.include_router(auth.router) +app.include_router(qrcode.router) app.include_router(desafios.router) origins = [ diff --git a/backend/app/models/atividade.py b/backend/app/models/atividade.py index d2d4286..9c51559 100644 --- a/backend/app/models/atividade.py +++ b/backend/app/models/atividade.py @@ -10,12 +10,8 @@ class Atividade(Base): id: Mapped[int] = mapped_column("id", Integer, primary_key=True) nome: Mapped[str] = mapped_column(String(255), nullable=False) pontos: Mapped[int] = mapped_column(Integer, default=0) - #empresa_id: Mapped[int] = mapped_column( - # ForeignKey(f"empresas.id", ondelete="CASCADE"), - # nullable=False - #) - - #empresa: Mapped["Empresas"] = relationship("Empresas", back_populates="atividades") + + empresa: Mapped[List["Empresas"]] = relationship("Empresas", back_populates="atividades") eventos: Mapped[List["Eventos"]] = relationship("Eventos", back_populates="atividade") desafios: Mapped[List["Desafios"]] = relationship("Desafios", back_populates="atividade") participacoes: Mapped[List["Participacao"]] = relationship("Participacao", back_populates="atividade") \ No newline at end of file diff --git a/backend/app/models/desafios.py b/backend/app/models/desafios.py index 9e6d7c6..0661964 100644 --- a/backend/app/models/desafios.py +++ b/backend/app/models/desafios.py @@ -11,10 +11,10 @@ class Desafios(Base): nullable=False ) descricao: Mapped[str] = mapped_column(Text, nullable=False) - #empresa_id: Mapped[int] = mapped_column( - # ForeignKey(f"empresas.id", ondelete="CASCADE"), - # nullable=False - #) + empresa_id: Mapped[int] = mapped_column( + ForeignKey(f"empresas.id", ondelete="CASCADE"), + nullable=False + ) atividade: Mapped["Atividade"] = relationship("Atividade", back_populates="desafios") - #empresa: Mapped["Empresas"] = relationship("Empresas", back_populates="desafios") \ No newline at end of file + empresa: Mapped["Empresas"] = relationship("Empresas", back_populates="desafios") \ No newline at end of file diff --git a/backend/app/models/empresas.py b/backend/app/models/empresas.py index 385f0c9..682a821 100644 --- a/backend/app/models/empresas.py +++ b/backend/app/models/empresas.py @@ -14,9 +14,14 @@ class Empresas(Base): nullable=False ) sponsor_type: Mapped[str] = mapped_column(String(50), nullable=True) + + atividade_id: Mapped[int] = mapped_column( + ForeignKey(f"atividade.id", ondelete="CASCADE"), + nullable=False + ) spotlight_time: Mapped[TIMESTAMP] = mapped_column(TIMESTAMP, nullable=True) user: Mapped["Users"] = relationship("Users", back_populates="empresas") - #atividades: Mapped[List["Atividade"]] = relationship("Atividade", back_populates="empresa") + atividades: Mapped["Atividade"] = relationship("Atividade", back_populates="empresa") eventos: Mapped[List["Eventos"]] = relationship("Eventos", back_populates="empresa") - #desafios: Mapped[List["Desafios"]] = relationship("Desafios", back_populates="empresa") \ No newline at end of file + desafios: Mapped[List["Desafios"]] = relationship("Desafios", back_populates="empresa") \ No newline at end of file diff --git a/backend/app/routes/desafios.py b/backend/app/routes/desafios.py index d79fa7b..499e4eb 100644 --- a/backend/app/routes/desafios.py +++ b/backend/app/routes/desafios.py @@ -43,11 +43,11 @@ def create_desafio( # return joined object return Desafio( - id=db_desafio.id, - nome=db_atividade.nome, - pontos=db_atividade.pontos, - descricao=db_desafio.descricao, - ) + id=db_desafio.id, + nome=db_atividade.nome, + pontos=db_atividade.pontos, + descricao=db_desafio.descricao, + ) except Exception as e: db.rollback() return HTTPException(status_code=400, detail=str(e)) @@ -57,7 +57,12 @@ def create_desafio( def read_desafios(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): # Join 'Atividade' and 'Desafios' tables db_desafios = ( - db.query(Desafios.id, Atividade.nome, Atividade.pontos, Desafios.descricao) + db.query( + Desafios.id, + Atividade.nome, + Atividade.pontos, + Desafios.descricao, + ) .join(Atividade, Desafios.atividade_id == Atividade.id) .offset(skip) .limit(limit) @@ -71,7 +76,12 @@ def read_desafios(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)) def read_desafio(desafio_id: int, db: Session = Depends(get_db)): # Join 'Atividade' and 'Desafios' tables db_desafio = ( - db.query(Desafios.id, Atividade.nome, Atividade.pontos, Desafios.descricao) + db.query( + Desafios.id, + Atividade.nome, + Atividade.pontos, + Desafios.descricao, + ) .join(Atividade, Desafios.atividade_id == Atividade.id) .filter(Desafios.id == desafio_id) .first() @@ -92,7 +102,12 @@ def update_desafio( # Join 'Atividade' and 'Desafios' tables db_desafio = ( - db.query(Desafios.id, Atividade.nome, Atividade.pontos, Desafios.descricao) + db.query( + Desafios.id, + Atividade.nome, + Atividade.pontos, + Desafios.descricao, + ) .join(Atividade, Desafios.atividade_id == Atividade.id) .filter(Desafios.id == desafio_id) .first() @@ -105,9 +120,9 @@ def update_desafio( {"nome": desafio.nome, "pontos": desafio.pontos} ) # update 'Desafios' object - db.query(Desafios).filter(Desafios.id == db_desafio.id).update( - {"descricao": desafio.descricao} - ) + db.query(Desafios).filter(Desafios.id == db_desafio.id).update({ + "descricao": desafio.descricao, + }) db.commit() # return updated object @@ -129,7 +144,12 @@ def delete_desafio( # Join 'Atividade' and 'Desafios' tables db_desafio = ( - db.query(Desafios.id, Atividade.nome, Atividade.pontos, Desafios.descricao) + db.query( + Desafios.id, + Atividade.nome, + Atividade.pontos, + Desafios.descricao, + ) .join(Atividade, Desafios.atividade_id == Atividade.id) .filter(Desafios.id == desafio_id) .first() diff --git a/backend/app/routes/qrcode.py b/backend/app/routes/qrcode.py new file mode 100644 index 0000000..af18f13 --- /dev/null +++ b/backend/app/routes/qrcode.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import StreamingResponse + +import io +import qrcode + +from app.dependencies import qrvalidation +from app.schemas.qrcode import QRCodeRequest + +router = APIRouter( + prefix="/qrcode", + tags=["qrcode"], + responses={404: {"description": "Not found"}} +) + +@router.post("/encode") +async def qrcode_encode(data: QRCodeRequest): + if not data: + raise HTTPException(status_code=400, detail="No data provided") + + # TODO: Use auth to validate user + + msg = data.msg + + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(qrvalidation.encode(msg)) + qr.make(fit=True) + + img = qr.make_image(fill='black', back_color='white') + img_io = io.BytesIO() + img.save(img_io, 'PNG') + img_io.seek(0) + + return StreamingResponse(img_io, media_type="image/png") + +@router.post("/decode") +async def qrcode_decode(data: QRCodeRequest): + if not data: + raise HTTPException(status_code=400, detail="No data provided") + + msg = data.msg + + userId = qrvalidation.decode(msg) + + if userId is None: + raise HTTPException(status_code=400, detail="Invalid QR code") + + return {"msg": userId} diff --git a/backend/app/schemas/atividades.py b/backend/app/schemas/atividades.py index a1640ff..c08b4d5 100644 --- a/backend/app/schemas/atividades.py +++ b/backend/app/schemas/atividades.py @@ -4,15 +4,3 @@ class AtividadeBase(BaseModel): nome: str pontos: Optional[int] = 0 - -class AtividadeCreate(AtividadeBase): - pass - -class AtividadeUpdate(AtividadeBase): - pass - -class Atividade(AtividadeBase): - id: int - - class Config: - from_attributes = True diff --git a/backend/app/schemas/desafios.py b/backend/app/schemas/desafios.py index aa1d040..410db29 100644 --- a/backend/app/schemas/desafios.py +++ b/backend/app/schemas/desafios.py @@ -4,7 +4,7 @@ class DesafioBase(AtividadeBase): descricao: str - #empresa_id: int + empresa_id: int class DesafioCreate(DesafioBase): pass diff --git a/backend/app/schemas/qrcode.py b/backend/app/schemas/qrcode.py new file mode 100644 index 0000000..fb8928c --- /dev/null +++ b/backend/app/schemas/qrcode.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + + +class QRCodeRequest(BaseModel): + msg: str diff --git a/backend/examples/README.md b/backend/examples/README.md new file mode 100644 index 0000000..3577bc5 --- /dev/null +++ b/backend/examples/README.md @@ -0,0 +1,5 @@ +# Examples + + These examples demonstrate the integration between different components of the system. + +**They only serve as support for project development and do not constitute an integral part of the system.** \ No newline at end of file diff --git a/backend/examples/qrcode/generate.html b/backend/examples/qrcode/generate.html new file mode 100644 index 0000000..ba03bb4 --- /dev/null +++ b/backend/examples/qrcode/generate.html @@ -0,0 +1,39 @@ + + + + QR Code Generator + + + +

QR Code Generator

+ + QR Code Scanner +

+
+ + +
+

+ QR Code will be displayed here + + diff --git a/backend/examples/qrcode/index.html b/backend/examples/qrcode/index.html new file mode 100644 index 0000000..cdcbdb6 --- /dev/null +++ b/backend/examples/qrcode/index.html @@ -0,0 +1,15 @@ + + + + QR Code + + +

QR Code

+ + QR Code Generator +

+ + QR Code Scanner +

+ + diff --git a/backend/examples/qrcode/scan.html b/backend/examples/qrcode/scan.html new file mode 100644 index 0000000..10da465 --- /dev/null +++ b/backend/examples/qrcode/scan.html @@ -0,0 +1,47 @@ + + + + QR Code Scanner + + + + +

QR Code Scanner

+
+
+

Scanned QR Code Result:

+

+
+ + \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index fd32a2e..ec2e247 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -38,3 +38,4 @@ uvicorn==0.30.5 uvloop==0.19.0 watchfiles==0.22.0 websockets==12.0 +qrcode==7.4.2 diff --git a/backend/sonar-project.properties b/backend/sonar-project.properties new file mode 100644 index 0000000..e800ec4 --- /dev/null +++ b/backend/sonar-project.properties @@ -0,0 +1,12 @@ +sonar.projectKey=d4d-backend +sonar.organization=aettua + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=d4d-backend +#sonar.projectVersion=1.0 + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 diff --git a/frontend/sonar-project.properties b/frontend/sonar-project.properties new file mode 100644 index 0000000..f22298f --- /dev/null +++ b/frontend/sonar-project.properties @@ -0,0 +1,12 @@ +sonar.projectKey=d4d-frontend +sonar.organization=aettua + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=d4d-frontend +#sonar.projectVersion=1.0 + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8