Skip to content
Open
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`.
28 changes: 15 additions & 13 deletions marktplaats-backend/src/marktplaats_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
"""
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",
"match_category_name",
"fetch_category_attributes",
"map_ai_attributes_to_marktplaats",
"upload_image_to_s3",
"get_marktplaats_access_token"
]
"get_marktplaats_access_token",
"get_authorization_url",
"exchange_code_for_tokens",
"refresh_user_access_token",
]
68 changes: 61 additions & 7 deletions marktplaats-backend/src/marktplaats_backend/marktplaats_auth.py
Original file line number Diff line number Diff line change
@@ -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"]

Expand All @@ -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())
30 changes: 30 additions & 0 deletions marktplaats-backend/tests/test_auth_flow.py
Original file line number Diff line number Diff line change
@@ -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()
13 changes: 13 additions & 0 deletions marktplaats-frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -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'
}
}
})
14 changes: 14 additions & 0 deletions marktplaats-frontend/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
12 changes: 12 additions & 0 deletions marktplaats-frontend/pages/callback.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Authorization Result</h1>
<p v-if="code">Code: {{ code }}</p>
<p v-else>Waiting for authorization...</p>
</div>
</template>

<script setup lang="ts">
const route = useRoute()
const code = route.query.code as string | undefined
</script>
15 changes: 15 additions & 0 deletions marktplaats-frontend/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Marktplaats Login</h1>
<a :href="loginUrl" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
Login with Marktplaats
</a>
</div>
</template>

<script setup lang="ts">
const config = useRuntimeConfig()
const scope = 'default'
const state = 'init'
const loginUrl = `${config.public.authBase}/authorize?response_type=code&client_id=${config.public.clientId}&redirect_uri=${encodeURIComponent(config.public.redirectUri)}&scope=${scope}&state=${state}`
</script>
12 changes: 12 additions & 0 deletions marktplaats-frontend/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Config } from 'tailwindcss'

export default <Config>{
content: [
'./components/**/*.{vue,js,ts}',
'./pages/**/*.{vue,js,ts}'
],
theme: {
extend: {}
},
plugins: []
}