diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb10a5f --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Marktplaatser + +This repository contains a backend AWS Lambda application and a minimal Nuxt PWA to authenticate users with the Marktplaats API. + +- **marktplaats-backend** – Python code for generating listings and interacting with the Marktplaats API. +- **marktplaats-frontend** – Nuxt 3 PWA with Tailwind CSS demonstrating the OAuth login flow. + +## OAuth Demo + +Set the following environment variables when running the frontend: + +```bash +export MARKTPLAATS_CLIENT_ID=your_client_id +export AUTH_REDIRECT_URI=http://localhost:3000/callback +``` + +Then run the frontend with `npm install` and `npm run dev` inside `marktplaats-frontend`. diff --git a/marktplaats-backend/src/marktplaats_backend/__init__.py b/marktplaats-backend/src/marktplaats_backend/__init__.py index 4ca626f..806626b 100644 --- a/marktplaats-backend/src/marktplaats_backend/__init__.py +++ b/marktplaats-backend/src/marktplaats_backend/__init__.py @@ -1,32 +1,31 @@ -""" -Marktplaats Backend - AWS Lambda application for generating marketplace listings from images. - -This package processes uploaded images through AWS Rekognition for object detection and text extraction, -then uses AWS Bedrock to generate structured listing data that matches Marktplaats categories and attributes. -""" +"""Marktplaats Backend package.""" __version__ = "1.0.0" __author__ = "Marktplaatser Team" -# Core modules from .generate_listing_lambda import lambda_handler from .rekognition_utils import extract_labels_and_text from .bedrock_utils import generate_listing_with_bedrock from .category_matcher import ( fetch_marktplaats_categories, flatten_categories, - match_category_name + match_category_name, ) from .attribute_mapper import ( fetch_category_attributes, - map_ai_attributes_to_marktplaats + map_ai_attributes_to_marktplaats, ) from .s3_utils import upload_image_to_s3 -from .marktplaats_auth import get_marktplaats_access_token +from .marktplaats_auth import ( + get_marktplaats_access_token, + get_authorization_url, + exchange_code_for_tokens, + refresh_user_access_token, +) __all__ = [ "lambda_handler", - "extract_labels_and_text", + "extract_labels_and_text", "generate_listing_with_bedrock", "fetch_marktplaats_categories", "flatten_categories", @@ -34,5 +33,8 @@ "fetch_category_attributes", "map_ai_attributes_to_marktplaats", "upload_image_to_s3", - "get_marktplaats_access_token" -] \ No newline at end of file + "get_marktplaats_access_token", + "get_authorization_url", + "exchange_code_for_tokens", + "refresh_user_access_token", +] diff --git a/marktplaats-backend/src/marktplaats_backend/marktplaats_auth.py b/marktplaats-backend/src/marktplaats_backend/marktplaats_auth.py index dfdad38..bc7d7d5 100644 --- a/marktplaats-backend/src/marktplaats_backend/marktplaats_auth.py +++ b/marktplaats-backend/src/marktplaats_backend/marktplaats_auth.py @@ -1,14 +1,21 @@ +"""Utility helpers for Marktplaats OAuth authentication.""" + import os +from typing import Dict + +import requests if os.environ.get("IS_LOCAL"): from dotenv import load_dotenv load_dotenv() -import requests +AUTH_BASE_URL = "https://auth.marktplaats.nl/accounts/oauth" + +def get_marktplaats_access_token() -> str: + """Retrieve an application access token using the client credentials flow.""" -def get_marktplaats_access_token(): client_id = os.environ["MARKTPLAATS_CLIENT_ID"] client_secret = os.environ["MARKTPLAATS_CLIENT_SECRET"] @@ -20,13 +27,60 @@ def get_marktplaats_access_token(): "client_secret": client_secret, } - response = requests.post( - "https://auth.marktplaats.nl/accounts/oauth/token", headers=headers, data=data - ) + response = requests.post(f"{AUTH_BASE_URL}/token", headers=headers, data=data) response.raise_for_status() return response.json()["access_token"] -# Optional test harness -if __name__ == "__main__": +def get_authorization_url(redirect_uri: str, state: str, scope: str = "default") -> str: + """Build the authorization URL for the user login flow.""" + + client_id = os.environ["MARKTPLAATS_CLIENT_ID"] + params = ( + f"response_type=code&client_id={client_id}&redirect_uri={redirect_uri}" + f"&scope={scope}&state={state}" + ) + return f"{AUTH_BASE_URL}/authorize?{params}" + + +def exchange_code_for_tokens(code: str, redirect_uri: str) -> Dict[str, str]: + """Exchange an authorization code for access and refresh tokens.""" + + client_id = os.environ["MARKTPLAATS_CLIENT_ID"] + client_secret = os.environ["MARKTPLAATS_CLIENT_SECRET"] + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "client_id": client_id, + "client_secret": client_secret, + } + + response = requests.post(f"{AUTH_BASE_URL}/token", headers=headers, data=data) + response.raise_for_status() + return response.json() + + +def refresh_user_access_token(refresh_token: str) -> Dict[str, str]: + """Refresh a user access token using a refresh token.""" + + client_id = os.environ["MARKTPLAATS_CLIENT_ID"] + client_secret = os.environ["MARKTPLAATS_CLIENT_SECRET"] + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + data = { + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": client_id, + "client_secret": client_secret, + } + + response = requests.post(f"{AUTH_BASE_URL}/token", headers=headers, data=data) + response.raise_for_status() + return response.json() + + +if __name__ == "__main__": # pragma: no cover - manual testing helper print(get_marktplaats_access_token()) diff --git a/marktplaats-backend/tests/test_auth_flow.py b/marktplaats-backend/tests/test_auth_flow.py new file mode 100644 index 0000000..655064d --- /dev/null +++ b/marktplaats-backend/tests/test_auth_flow.py @@ -0,0 +1,30 @@ +import os +import sys +from unittest.mock import patch, MagicMock + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src", "marktplaats_backend")) + +import marktplaats_auth as auth + + +def test_get_authorization_url(): + os.environ["MARKTPLAATS_CLIENT_ID"] = "client-id" + url = auth.get_authorization_url("http://localhost/callback", "abc", "default") + assert "client_id=client-id" in url + assert "redirect_uri=http://localhost/callback" in url + assert url.startswith(auth.AUTH_BASE_URL) + + +@patch("marktplaats_backend.marktplaats_auth.requests.post") +def test_exchange_code_for_tokens(mock_post): + os.environ["MARKTPLAATS_CLIENT_ID"] = "client-id" + os.environ["MARKTPLAATS_CLIENT_SECRET"] = "secret" + mock_resp = MagicMock() + mock_resp.json.return_value = {"access_token": "token", "refresh_token": "ref"} + mock_resp.raise_for_status.return_value = None + mock_post.return_value = mock_resp + + tokens = auth.exchange_code_for_tokens("code", "http://localhost/callback") + + assert tokens["access_token"] == "token" + mock_post.assert_called_once() diff --git a/marktplaats-frontend/nuxt.config.ts b/marktplaats-frontend/nuxt.config.ts new file mode 100644 index 0000000..3c3951e --- /dev/null +++ b/marktplaats-frontend/nuxt.config.ts @@ -0,0 +1,13 @@ +import { defineNuxtConfig } from 'nuxt/config' + +export default defineNuxtConfig({ + ssr: false, + modules: ['@nuxtjs/tailwindcss'], + runtimeConfig: { + public: { + clientId: process.env.MARKTPLAATS_CLIENT_ID, + redirectUri: process.env.AUTH_REDIRECT_URI, + authBase: 'https://auth.marktplaats.nl/accounts/oauth' + } + } +}) diff --git a/marktplaats-frontend/package.json b/marktplaats-frontend/package.json new file mode 100644 index 0000000..5c643d2 --- /dev/null +++ b/marktplaats-frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "marktplaats-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "start": "nuxt start" + }, + "dependencies": { + "nuxt": "^3.10.0", + "@nuxtjs/tailwindcss": "^6.9.1" + } +} diff --git a/marktplaats-frontend/pages/callback.vue b/marktplaats-frontend/pages/callback.vue new file mode 100644 index 0000000..732034e --- /dev/null +++ b/marktplaats-frontend/pages/callback.vue @@ -0,0 +1,12 @@ + + + diff --git a/marktplaats-frontend/pages/index.vue b/marktplaats-frontend/pages/index.vue new file mode 100644 index 0000000..d3a4131 --- /dev/null +++ b/marktplaats-frontend/pages/index.vue @@ -0,0 +1,15 @@ + + + diff --git a/marktplaats-frontend/tailwind.config.ts b/marktplaats-frontend/tailwind.config.ts new file mode 100644 index 0000000..15d10a6 --- /dev/null +++ b/marktplaats-frontend/tailwind.config.ts @@ -0,0 +1,12 @@ +import type { Config } from 'tailwindcss' + +export default { + content: [ + './components/**/*.{vue,js,ts}', + './pages/**/*.{vue,js,ts}' + ], + theme: { + extend: {} + }, + plugins: [] +}