Skip to content

Commit c55ba4d

Browse files
committed
Initial working version
0 parents  commit c55ba4d

File tree

8 files changed

+318
-0
lines changed

8 files changed

+318
-0
lines changed

.github/workflows/publish.yml

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Publish Python Package
2+
3+
on:
4+
release:
5+
types: [created]
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
python-version: [3.6, 3.7, 3.8, 3.9]
13+
steps:
14+
- uses: actions/checkout@v2
15+
- name: Set up Python ${{ matrix.python-version }}
16+
uses: actions/setup-python@v2
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
- uses: actions/cache@v2
20+
name: Configure pip caching
21+
with:
22+
path: ~/.cache/pip
23+
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
24+
restore-keys: |
25+
${{ runner.os }}-pip-
26+
- name: Install dependencies
27+
run: |
28+
pip install -e '.[test]'
29+
- name: Run tests
30+
run: |
31+
pytest
32+
deploy:
33+
runs-on: ubuntu-latest
34+
needs: [test]
35+
steps:
36+
- uses: actions/checkout@v2
37+
- name: Set up Python
38+
uses: actions/setup-python@v2
39+
with:
40+
python-version: '3.9'
41+
- uses: actions/cache@v2
42+
name: Configure pip caching
43+
with:
44+
path: ~/.cache/pip
45+
key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }}
46+
restore-keys: |
47+
${{ runner.os }}-publish-pip-
48+
- name: Install dependencies
49+
run: |
50+
pip install setuptools wheel twine
51+
- name: Publish
52+
env:
53+
TWINE_USERNAME: __token__
54+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
55+
run: |
56+
python setup.py sdist bdist_wheel
57+
twine upload dist/*
58+

.github/workflows/test.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Test
2+
3+
on: [push]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: [3.6, 3.7, 3.8, 3.9]
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Set up Python ${{ matrix.python-version }}
14+
uses: actions/setup-python@v2
15+
with:
16+
python-version: ${{ matrix.python-version }}
17+
- uses: actions/cache@v2
18+
name: Configure pip caching
19+
with:
20+
path: ~/.cache/pip
21+
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
22+
restore-keys: |
23+
${{ runner.os }}-pip-
24+
- name: Install dependencies
25+
run: |
26+
pip install -e '.[test]'
27+
- name: Run tests
28+
run: |
29+
pytest
30+

.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.venv
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
venv
6+
.eggs
7+
.pytest_cache
8+
*.egg-info
9+
.DS_Store
10+
.vscode

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# datasette-indieauth
2+
3+
[![PyPI](https://img.shields.io/pypi/v/datasette-indieauth.svg)](https://pypi.org/project/datasette-indieauth/)
4+
[![Changelog](https://img.shields.io/github/v/release/simonw/datasette-indieauth?include_prereleases&label=changelog)](https://github.com/simonw/datasette-indieauth/releases)
5+
[![Tests](https://github.com/simonw/datasette-indieauth/workflows/Test/badge.svg)](https://github.com/simonw/datasette-indieauth/actions?query=workflow%3ATest)
6+
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/simonw/datasette-indieauth/blob/main/LICENSE)
7+
8+
**Alpha**. Datasette authentication using [IndieAuth](https://indieauth.net/) and [RelMeAuth](http://microformats.org/wiki/RelMeAuth).
9+
10+
This initial version depends on [IndieAuth.com](https://indieauth.com/).
11+
12+
## Installation
13+
14+
Install this plugin in the same environment as Datasette.
15+
16+
$ datasette install datasette-indieauth
17+
18+
## Usage
19+
20+
Ensure you have a website with a domain that supports IndieAuth or RelMeAuth.
21+
22+
Visit `/-/indieauth` to begin the sign-in progress.
23+
24+
## Development
25+
26+
To set up this plugin locally, first checkout the code. Then create a new virtual environment:
27+
28+
cd datasette-indieauth
29+
python3 -mvenv venv
30+
source venv/bin/activate
31+
32+
Or if you are using `pipenv`:
33+
34+
pipenv shell
35+
36+
Now install the dependencies and tests:
37+
38+
pip install -e '.[test]'
39+
40+
To run the tests:
41+
42+
pytest

datasette_indieauth/__init__.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from datasette import hookimpl
2+
from datasette.utils.asgi import Response
3+
import httpx
4+
import urllib
5+
6+
7+
async def indieauth(request, datasette):
8+
client_id = datasette.absolute_url(request, datasette.urls.instance())
9+
redirect_uri = datasette.absolute_url(request, request.path)
10+
11+
if request.args.get("code") and request.args.get("me"):
12+
ok, extra = await verify_code(request.args["code"], client_id, redirect_uri)
13+
if ok:
14+
response = Response.redirect(datasette.urls.instance())
15+
response.set_cookie(
16+
"ds_actor",
17+
datasette.sign(
18+
{
19+
"a": {
20+
"me": extra,
21+
"display": extra,
22+
}
23+
},
24+
"actor",
25+
),
26+
)
27+
return response
28+
else:
29+
return Response.text(extra, status=403)
30+
31+
return Response.html(
32+
await datasette.render_template(
33+
"indieauth.html",
34+
{
35+
"client_id": client_id,
36+
"redirect_uri": redirect_uri,
37+
},
38+
request=request,
39+
)
40+
)
41+
42+
43+
async def verify_code(code, client_id, redirect_uri):
44+
async with httpx.AsyncClient() as client:
45+
response = await client.post(
46+
"https://indieauth.com/auth",
47+
data={
48+
"code": code,
49+
"client_id": client_id,
50+
"redirect_uri": redirect_uri,
51+
},
52+
)
53+
if response.status_code == 200:
54+
# me=https%3A%2F%2Fsimonwillison.net%2F&scope
55+
bits = dict(urllib.parse.parse_qsl(response.text))
56+
if "me" in bits:
57+
return True, bits["me"]
58+
else:
59+
return False, "Server did not return me="
60+
else:
61+
return False, "{}: {}".format(response.status_code, response.text)
62+
63+
64+
@hookimpl
65+
def register_routes():
66+
return [
67+
(r"^/-/indieauth$", indieauth),
68+
]
69+
70+
71+
@hookimpl
72+
def menu_links(datasette, actor):
73+
if not actor:
74+
return [
75+
{
76+
"href": datasette.urls.path("/-/indieauth"),
77+
"label": "Sign in IndieAuth",
78+
},
79+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}Sign in with IndieAuth{% endblock %}
4+
5+
{% block content %}
6+
<h1>Sign in with IndieAuth</h1>
7+
<form action="https://indieauth.com/auth" method="get">
8+
<p><input type="text" name="me">
9+
<input type="hidden" name="client_id" value="{{ client_id }}">
10+
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
11+
<input type="submit" value="Login">
12+
</p>
13+
</form>
14+
{% endblock %}

setup.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from setuptools import setup
2+
import os
3+
4+
VERSION = "0.1a0"
5+
6+
7+
def get_long_description():
8+
with open(
9+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.md"),
10+
encoding="utf8",
11+
) as fp:
12+
return fp.read()
13+
14+
15+
setup(
16+
name="datasette-indieauth",
17+
description="Datasette authentication using IndieAuth and RelMeAuth",
18+
long_description=get_long_description(),
19+
long_description_content_type="text/markdown",
20+
author="Simon Willison",
21+
url="https://github.com/simonw/datasette-indieauth",
22+
project_urls={
23+
"Issues": "https://github.com/simonw/datasette-indieauth/issues",
24+
"CI": "https://github.com/simonw/datasette-indieauth/actions",
25+
"Changelog": "https://github.com/simonw/datasette-indieauth/releases",
26+
},
27+
license="Apache License, Version 2.0",
28+
version=VERSION,
29+
packages=["datasette_indieauth"],
30+
entry_points={"datasette": ["indieauth = datasette_indieauth"]},
31+
install_requires=["datasette"],
32+
extras_require={"test": ["pytest", "pytest-asyncio", "httpx", "pytest-httpx"]},
33+
tests_require=["datasette-indieauth[test]"],
34+
package_data={"datasette_indieauth": ["templates/*.html"]},
35+
python_requires=">=3.6",
36+
)

tests/test_indieauth.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from datasette.app import Datasette
2+
import pytest
3+
import httpx
4+
5+
6+
@pytest.fixture
7+
def non_mocked_hosts():
8+
return ["localhost"]
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_plugin_is_installed():
13+
app = Datasette([], memory=True).app()
14+
async with httpx.AsyncClient(app=app) as client:
15+
response = await client.get("http://localhost/-/plugins.json")
16+
assert 200 == response.status_code
17+
installed_plugins = {p["name"] for p in response.json()}
18+
assert "datasette-indieauth" in installed_plugins
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_auth_succeeds(httpx_mock):
23+
httpx_mock.add_response(url="https://indieauth.com/auth", data=b"me=example.com")
24+
datasette = Datasette([], memory=True)
25+
app = datasette.app()
26+
async with httpx.AsyncClient(app=app) as client:
27+
response = await client.get(
28+
"http://localhost/-/indieauth?code=code&me=example.com",
29+
allow_redirects=False,
30+
)
31+
# Should set a cookie
32+
assert response.status_code == 302
33+
assert datasette.unsign(response.cookies["ds_actor"], "actor") == {
34+
"a": {"me": "example.com", "display": "example.com"}
35+
}
36+
37+
38+
@pytest.mark.asyncio
39+
async def test_auth_fails(httpx_mock):
40+
httpx_mock.add_response(url="https://indieauth.com/auth", status_code=404)
41+
datasette = Datasette([], memory=True)
42+
app = datasette.app()
43+
async with httpx.AsyncClient(app=app) as client:
44+
response = await client.get(
45+
"http://localhost/-/indieauth?code=code&me=example.com",
46+
allow_redirects=False,
47+
)
48+
# Should return error
49+
assert response.status_code == 403

0 commit comments

Comments
 (0)