diff --git a/ckan-backend-dev/.env.example b/ckan-backend-dev/.env.example
index 2723f2950..037f448fb 100644
--- a/ckan-backend-dev/.env.example
+++ b/ckan-backend-dev/.env.example
@@ -104,3 +104,7 @@ CKAN___SCHEMING__PRESETS=ckanext.wri.schema:presets.json
# auth
CKANEXT__AUTH__INCLUDE_FRONTEND_LOGIN_TOKEN=True
+
+# custom auth
+CKANEXT__WRI__ODP_URL=http://localhost:3000
+
diff --git a/ckan-backend-dev/docker-compose.dev.yml b/ckan-backend-dev/docker-compose.dev.yml
index 514db5159..cc6a53eb7 100755
--- a/ckan-backend-dev/docker-compose.dev.yml
+++ b/ckan-backend-dev/docker-compose.dev.yml
@@ -46,6 +46,7 @@ services:
- S3_SECRET_KEY_ID=${MINIO_ROOT_PASSWORD}
- S3_BUCKET_NAME=ckan
- S3_BUCKET_REGION=us-east-1
+ - SYS_ADMIN_API_KEY=${CKAN__DATAPUSHER__API_TOKEN}
environment:
- NEXTAUTH_SECRET=secret
- NEXTAUTH_URL=http://localhost:3000
@@ -54,6 +55,7 @@ services:
- S3_SECRET_KEY_ID=${MINIO_ROOT_PASSWORD}
- S3_BUCKET_NAME=ckan
- S3_BUCKET_REGION=us-east-1
+ - SYS_ADMIN_API_KEY=${CKAN__DATAPUSHER__API_TOKEN}
ports:
- "0.0.0.0:3000:3000"
healthcheck:
diff --git a/ckan-backend-dev/docker-compose.test.yml b/ckan-backend-dev/docker-compose.test.yml
index 0712228e5..96060410c 100755
--- a/ckan-backend-dev/docker-compose.test.yml
+++ b/ckan-backend-dev/docker-compose.test.yml
@@ -52,6 +52,7 @@ services:
- S3_SECRET_KEY_ID=${MINIO_ROOT_PASSWORD}
- S3_BUCKET_NAME=ckan
- S3_BUCKET_REGION=us-east-1
+ - SYS_ADMIN_API_KEY=${CKAN__DATAPUSHER__API_TOKEN}
ports:
- "0.0.0.0:3000:3000"
healthcheck:
diff --git a/ckan-backend-dev/src/ckanext-wri/ckanext/wri/lib/mailer.py b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/lib/mailer.py
new file mode 100644
index 000000000..35a8b5f00
--- /dev/null
+++ b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/lib/mailer.py
@@ -0,0 +1,31 @@
+from ckan.lib.mailer import create_reset_key, mail_user, MailerException
+from ckan.lib.base import render
+from ckan.common import config
+import ckan.model as model
+
+def get_reset_link(user: model.User) -> str:
+ odp_url = config.get('ckanext.wri.odp_url')
+ return "{}/auth/password-reset?token={}&user_id={}".format(odp_url, user.reset_key, user.id)
+
+def get_reset_link_body(user: model.User) -> str:
+ extra_vars = {
+ 'reset_link': get_reset_link(user),
+ 'site_title': "WRI Open Data Portal",
+ 'site_url': config.get('ckanext.wri.odp_url'),
+ 'user_name': user.name,
+ }
+ # NOTE: This template is translated
+ return render('emails/reset_password.txt', extra_vars)
+
+def send_reset_link(user: model.User) -> None:
+ create_reset_key(user)
+ body = get_reset_link_body(user)
+ extra_vars = {
+ 'site_title': config.get('ckan.site_title')
+ }
+ subject = render('emails/reset_password_subject.txt', extra_vars)
+
+ # Make sure we only use the first line
+ subject = subject.split('\n')[0]
+
+ mail_user(user, subject, body, body)
diff --git a/ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/action.py b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/action.py
new file mode 100644
index 000000000..33c7d50de
--- /dev/null
+++ b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/action.py
@@ -0,0 +1,35 @@
+import ckan.plugins.toolkit as tk
+import ckan.logic as logic
+import ckanext.wri.lib.mailer as mailer
+from ckan.types import Context
+from ckan.common import _
+
+
+ValidationError = logic.ValidationError
+
+import logging
+
+log = logging.getLogger(__name__)
+
+def password_reset(context: Context, data_dict: [str, any]):
+ email = data_dict.get("email", False)
+
+ if not email:
+ raise ValidationError({"email": [_("Please provide an email address")]})
+ model = context['model']
+ session = context['session']
+
+ user = session.query(model.User).filter_by(email=email).all()
+
+ if not user:
+ # Do not leak whether the email is registered or not
+ return "Password reset link sent to email address"
+
+ try:
+ mailer.send_reset_link(user[0])
+ return "Password reset link sent to email address"
+ except mailer.MailerException as e:
+ log.exception(e)
+ return "Password reset link sent to email address"
+
+
diff --git a/ckan-backend-dev/src/ckanext-wri/ckanext/wri/plugin.py b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/plugin.py
index b93ab5250..ee82da28e 100644
--- a/ckan-backend-dev/src/ckanext-wri/ckanext/wri/plugin.py
+++ b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/plugin.py
@@ -1,12 +1,14 @@
import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
+import ckanext.wri.logic.action as action
from ckanext.wri.logic.validators import iso_language_code
class WriPlugin(plugins.SingletonPlugin):
plugins.implements(plugins.IConfigurer)
plugins.implements(plugins.IValidators)
+ plugins.implements(plugins.IActions)
# IConfigurer
@@ -21,3 +23,12 @@ def get_validators(self):
return {
"iso_language_code": iso_language_code
}
+
+ # IActions
+
+ def get_actions(self):
+ return {
+ "password_reset": action.password_reset
+
+ }
+
diff --git a/ckan-backend-dev/src/ckanext-wri/ckanext/wri/templates/emails/reset_password.txt b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/templates/emails/reset_password.txt
new file mode 100644
index 000000000..259d72d22
--- /dev/null
+++ b/ckan-backend-dev/src/ckanext-wri/ckanext/wri/templates/emails/reset_password.txt
@@ -0,0 +1,23 @@
+{% trans %}
+
+Dear {{ user_name }},
+
+
+You have requested your password on {{ site_title }} to be reset.
+
+
+Please click the following link to confirm this request:
+
+
+ Reset password
+
+
+Have a nice day.
+
+
+--
+Message sent by {{ site_title }} ({{ site_url }})
+
+{% endtrans %}
+
+
diff --git a/deployment/frontend/.env.example b/deployment/frontend/.env.example
index ca4b64994..b6df911ee 100644
--- a/deployment/frontend/.env.example
+++ b/deployment/frontend/.env.example
@@ -24,3 +24,4 @@ S3_ACCESS_KEY_ID="minioadmin"
S3_SECRET_KEY_ID="minioadmin"
S3_BUCKET_NAME="ckan"
S3_BUCKET_REGION="us-east-1"
+SYS_ADMIN_API_KEY="1111"
diff --git a/deployment/frontend/package-lock.json b/deployment/frontend/package-lock.json
index 85c5db9b8..5406946ff 100644
--- a/deployment/frontend/package-lock.json
+++ b/deployment/frontend/package-lock.json
@@ -1714,66 +1714,6 @@
"glob": "7.1.7"
}
},
- "node_modules/@next/swc-darwin-arm64": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
- "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-darwin-x64": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
- "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-arm64-gnu": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
- "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-arm64-musl": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
- "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.5.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz",
@@ -1804,51 +1744,6 @@
"node": ">= 10"
}
},
- "node_modules/@next/swc-win32-arm64-msvc": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
- "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-ia32-msvc": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
- "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
- "cpu": [
- "ia32"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-x64-msvc": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
- "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -6803,19 +6698,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "hasInstallScript": true,
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -12584,6 +12466,111 @@
"optional": true
}
}
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
+ "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
+ "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
+ "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
+ "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
+ "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
+ "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
+ "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
}
}
}
diff --git a/deployment/frontend/src/components/_shared/Header.tsx b/deployment/frontend/src/components/_shared/Header.tsx
index b2fd53b6a..4fa4d0367 100644
--- a/deployment/frontend/src/components/_shared/Header.tsx
+++ b/deployment/frontend/src/components/_shared/Header.tsx
@@ -1,175 +1,191 @@
-import React, { Fragment, useState } from "react";
-import Image from "next/image";
-import { Menu, Transition, Dialog } from "@headlessui/react";
-import { Bars3Icon } from "@heroicons/react/20/solid";
-import Link from "next/link";
-import { useRouter } from "next/router";
-import Login from "./Login";
-import UserMenu from "./UserMenu";
+import React, { Fragment, useState } from 'react'
+import Image from 'next/image'
+import { Menu, Transition, Dialog } from '@headlessui/react'
+import { Bars3Icon } from '@heroicons/react/20/solid'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import Login from './Login'
+import UserMenu from './UserMenu'
+import {useSession} from 'next-auth/react'
export default function Header() {
- const { asPath } = useRouter();
- const [isOpen, setIsOpen] = useState(false)
+ const { asPath } = useRouter()
+ const [isOpen, setIsOpen] = useState(false)
+ const session = useSession()
- function closeModal() {
- setIsOpen(false)
- }
+ function closeModal() {
+ setIsOpen(false)
+ }
- function openModal() {
- setIsOpen(true)
- }
+ function openModal() {
+ setIsOpen(true)
+ }
+ const navigation = [
+ {
+ title: 'Search',
+ href: '/search',
+ active: false,
+ },
+ {
+ title: 'Teams',
+ href: '/teams',
+ active: false,
+ },
+ {
+ title: 'Topics',
+ href: '/topics',
+ active: false,
+ },
+ {
+ title: 'About',
+ href: '/about',
+ active: false,
+ },
+ ]
- const navigation = [
- {
- title: "Search",
- href: "/search",
- active: false,
- },
- {
- title: "Teams",
- href: "/teams",
- active: false,
- },
- {
- title: "Topics",
- href: "/topics",
- active: false,
- },
- {
- title: "About",
- href: "/about",
- active: false,
- },
- ];
-
- navigation.forEach((item) => {
- item.active = asPath.startsWith(item.href);
- });
-
- return (
-