Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
AWS_REGION: ${{ secrets.AWS_REGION }}
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
LAUNCH_DARKLY_KEY: ${{ secrets.LAUNCH_DARKLY_KEY_DEV }}
DB_HOST: 127.0.0.1 # Will not work with 'localhost', since that will try a Unix socket connection (!)
services:
elasticsearch7:
image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0
Expand All @@ -30,6 +31,16 @@ jobs:
http.cors.allow-origin: "*"
ports:
- 9200:9200
db:
image: mysql:8.0
env:
MYSQL_DATABASE: "rorapi"
MYSQL_USER: "ror_user"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout ror-api code
uses: actions/checkout@v2
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
ELASTIC_PASSWORD: "changeme"
ELASTIC7_HOST: "localhost"
ELASTIC7_PORT: "9200"
DB_HOST: 127.0.0.1
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
Expand All @@ -26,6 +27,16 @@ jobs:
http.cors.allow-origin: "*"
ports:
- 9200:9200
db:
image: mysql:8.0
env:
MYSQL_DATABASE: "rorapi"
MYSQL_USER: "ror_user"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout ror-api code
uses: actions/checkout@v2
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
ELASTIC_PASSWORD: "changeme"
ELASTIC7_HOST: "localhost"
ELASTIC7_PORT: "9200"
DB_HOST: 127.0.0.1
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
Expand All @@ -27,6 +28,16 @@ jobs:
http.cors.allow-origin: "*"
ports:
- 9200:9200
db:
image: mysql:8.0
env:
MYSQL_DATABASE: "rorapi"
MYSQL_USER: "ror_user"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Checkout ror-api code
uses: actions/checkout@v2
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ RUN mv /etc/apt/sources.list.d /etc/apt/sources.list.d.bak && \
mv /etc/apt/sources.list.d.bak /etc/apt/sources.list.d && \
apt-get upgrade -y -o Dpkg::Options::="--force-confold" && \
apt-get clean && \
apt-get install ntp wget unzip tzdata python3-pip libmagic1 -y && \
apt-get install ntp wget unzip tzdata python3-pip libmagic1 default-libmysqlclient-dev libcairo2-dev pkg-config -y && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Enable Passenger and Nginx and remove the default site
Expand Down Expand Up @@ -54,6 +54,7 @@ RUN pip3 install --no-cache-dir -r requirements.txt
RUN pip3 install yapf

# collect static files for Django
ENV DJANGO_SKIP_DB_CHECK=True
RUN python manage.py collectstatic --noinput

# Expose web
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Commands for indexing ROR data, generating new ROR IDs and other internal operat
ROUTE_USER=[USER]
TOKEN=[TOKEN]

Replace values in [] with valid credential values. GITHUB_TOKEN is needed in order to index an existing data dump locally. ROUTE_USER and TOKEN are only needed in order to use generate-id functionality locally. AWS_* and DATA_STORE are only needed in order to use incremental indexing from S3 functionality locally.
ROR staff should replace values in [] with valid credential values. External users do not need to add these values but should comment out this line https://github.com/ror-community/ror-api/blob/8a5a5ae8b483564c966a7184349c581dcae756ef/rorapi/management/commands/setup.py#L13 so that there is no attempt to send a Github token when retrieving a data dump for indexing.

- Optionally, uncomment [line 24 in docker-compose.yml](https://github.com/ror-community/ror-api/blob/master/docker-compose.yml#L24) in order to pull the rorapi image from Dockerhub rather than creating it from local code

## Start ror-api locally
1. Start Docker Desktop
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ services:
timeout: 1s
volumes:
- ./esdata:/usr/share/elasticsearch/data
db:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
env_file:
- .env
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
web:
container_name: rorapiweb
env_file: .env
Expand All @@ -31,3 +43,6 @@ services:
- ./rorapi:/home/app/webapp/rorapi
depends_on:
- elasticsearch7
- db
volumes:
mysql_data:
6 changes: 5 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ update_address @ git+https://github.com/ror-community/update_address.git
launchdarkly-server-sdk==7.6.1
jsonschema==3.2.0
python-magic
iso639-lang
iso639-lang
mysqlclient==2.2.7
bleach==6.0.0
pycountry==22.3.5
django-ses==3.5.0
4 changes: 3 additions & 1 deletion rorapi/common/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rest_framework.documentation import include_docs_urls
from . import views
from rorapi.common.views import (
HeartbeatView,GenerateAddress,GenerateId,IndexData,IndexDataDump,BulkUpdate)
HeartbeatView,GenerateAddress,GenerateId,IndexData,IndexDataDump,BulkUpdate,ClientRegistrationView,ValidateClientView)

urlpatterns = [
# Health check
Expand All @@ -14,6 +14,8 @@
path('generateaddress/<str:geonamesid>', GenerateAddress.as_view()),
url(r"^generateid$", GenerateId.as_view()),
re_path(r"^(?P<version>(v1|v2))\/bulkupdate$", BulkUpdate.as_view()),
re_path(r"^(?P<version>(v1|v2))\/register$", ClientRegistrationView.as_view()),
path('validate-client-id/<str:client_id>/', ValidateClientView.as_view()),
url(r"^(?P<version>(v1|v2))\/indexdata/(?P<branch>.*)", IndexData.as_view()),
url(r"^(?P<version>(v1|v2))\/indexdatadump\/(?P<filename>v(\d+\.)?(\d+\.)?(\*|\d+)-\d{4}-\d{2}-\d{2}-ror-data)\/(?P<dataenv>(test|prod))$", IndexDataDump.as_view()),
url(r"^(?P<version>(v1|v2))\/", include(views.organizations_router.urls)),
Expand Down
79 changes: 78 additions & 1 deletion rorapi/common/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,87 @@
import os
import update_address as ua
from rorapi.management.commands.generaterorid import check_ror_id
from rorapi.management.commands.generaterorid import check_ror_id
from rorapi.management.commands.indexror import process_files
from django.core import management
import rorapi.management.commands.indexrordump
from django.core.mail import EmailMultiAlternatives
from django.utils.timezone import now
from rorapi.v2.models import Client
from rorapi.v2.serializers import ClientSerializer

class ClientRegistrationView(APIView):
def post(self, request, version='v2'):
serializer = ClientSerializer(data=request.data)
if serializer.is_valid():
client = serializer.save()

subject = 'ROR API client ID'
from_email = "ROR API Support <api@ror.org>"
recipient_list = [client.email]

html_content = self._get_html_content(client.client_id)
text_content = self._get_text_content(client.client_id)

msg = EmailMultiAlternatives(subject, text_content, from_email, recipient_list)
msg.attach_alternative(html_content, "text/html")
msg.send()

return Response({'client_id': client.client_id}, status=status.HTTP_201_CREATED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def _get_text_content(self, client_id):
return f"""
Thank you for registering for a ROR API client ID!

Your ROR API client ID is:
{client_id}

This client ID is not used for authentication or authorization, and is therefore not secret and can be sent as plain text.

In order to receive a rate limit of 2000 requests per 5 minute period, please include this client ID with your ROR API requests, in a custom HTTP header named Client-Id, for example:

curl -H "Client-Id: {client_id}" https://api.ror.org/organizations?query=oxford

Requests without a valid client ID are subject to a rate limit of 50 requests per 5 minute period.

We do not provide a way to recover or revoke a lost client ID. If you lose track of your client ID, please register a new client ID. For more information about ROR API client IDs, see https://ror.readme.io/docs/client-id

If you have questions, please see ROR documentation or contact us at support@ror.org

Cheers,
The ROR Team
support@ror.org
https://ror.org
"""


def _get_html_content(self, client_id):
return f"""
<div style="font-family: Arial, sans-serif; line-height: 1.5;">
<p>Thank you for registering for a ROR API client ID!</p>
<p><strong>Your ROR API client ID is:</strong></p>
<pre style="background:#f4f4f4;padding:10px;">{client_id}</pre>
<p>This client ID is not used for authentication or authorization, and is therefore not secret and can be sent as plain text.</p>
<p>In order to receive a rate limit of <strong>2000 requests per 5 minute period</strong>, please include this client ID with your ROR API requests, in a custom HTTP header named <code>Client-Id</code>, for example:</p>
<pre style="background:#f4f4f4;padding:10px;">curl -H "Client-Id: {client_id}" https://api.ror.org/organizations?query=oxford</pre>
<p>Requests without a valid client ID are subject to a rate limit of 50 requests per 5 minute period.</p>
<p>We do not provide a way to recover or revoke a lost client ID. If you lose track of your client ID, please register a new one.</p>
<p>For more information about ROR API client IDs, see <a href="https://ror.readme.io/docs/client-id/">our documentation</a>.</p>
<p>If you have questions, please see the ROR documentation or contact us at <a href="mailto:support@ror.org">support@ror.org</a>.</p>
<p>Cheers,<br>
The ROR Team<br>
<a href="mailto:support@ror.org">support@ror.org</a><br>
<a href="https://ror.org">https://ror.org</a></p>
</div>
"""


class ValidateClientView(APIView):
def get(self, request, client_id):
client_exists = Client.objects.filter(client_id=client_id).exists()

return Response({'valid': client_exists}, status=status.HTTP_200_OK)

class OurTokenPermission(BasePermission):
"""
Expand Down
7 changes: 7 additions & 0 deletions rorapi/management/commands/generaterorid.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ def check_ror_id(version):
check_ror_id(version)
return ror_id


def generate_ror_client_id():
"""Generates a random ROR client ID.
"""

n = random.randint(0, 2**160 - 1)
return base32_crockford.encode(n).lower().zfill(32)
30 changes: 30 additions & 0 deletions rorapi/migrations/0001_create_client_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.2.28 on 2025-03-11 07:13

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Client',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=255)),
('name', models.CharField(blank=True, max_length=255)),
('institution_name', models.CharField(blank=True, max_length=255)),
('institution_ror', models.URLField(blank=True, max_length=255)),
('country_code', models.CharField(blank=True, max_length=2)),
('ror_use', models.TextField(blank=True, max_length=500)),
('client_id', models.CharField(editable=False, max_length=32, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('last_request_at', models.DateTimeField(blank=True, null=True)),
('request_count', models.IntegerField(default=0)),
],
),
]
18 changes: 18 additions & 0 deletions rorapi/migrations/0002_auto_20250326_1054.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2025-03-26 10:54

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('rorapi', '0001_create_client_model'),
]

operations = [
migrations.AlterField(
model_name='client',
name='email',
field=models.EmailField(max_length=255, unique=True),
),
]
43 changes: 43 additions & 0 deletions rorapi/migrations/0003_auto_20250415_1207.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 2.2.28 on 2025-04-15 12:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('rorapi', '0002_auto_20250326_1054'),
]

operations = [
migrations.AlterField(
model_name='client',
name='country_code',
field=models.CharField(blank=True, max_length=2, null=True),
),
migrations.AlterField(
model_name='client',
name='email',
field=models.EmailField(max_length=255),
),
migrations.AlterField(
model_name='client',
name='institution_name',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='client',
name='institution_ror',
field=models.URLField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='client',
name='name',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='client',
name='ror_use',
field=models.TextField(blank=True, max_length=500, null=True),
),
]
Empty file added rorapi/migrations/__init__.py
Empty file.
Loading
Loading