From fcc979255543d529b15277fac243836bebe8da9c Mon Sep 17 00:00:00 2001 From: Hadley King Date: Fri, 9 Jun 2023 15:29:32 -0400 Subject: [PATCH] 23.07 (#121) * GA4 added * Add Orcid User Info Api Fix https://github.com/biocompute-objects/playbook-partnership/issues/16 Changes to be committed: modified: server/authentication/apis.py modified: server/authentication/services.py modified: server/authentication/urls.py * Typo fix Changes to be committed: modified: client/src/components/bcodbs/SearchOptions.js * Update services.py move `now = make_aware(datetime.utcnow())` to top * Security fixes (#115) * Add Orcid User Info Api Fix https://github.com/biocompute-objects/playbook-partnership/issues/16 Changes to be committed: modified: server/authentication/apis.py modified: server/authentication/services.py modified: server/authentication/urls.py * Typo fix Changes to be committed: modified: client/src/components/bcodbs/SearchOptions.js * Update services.py move `now = make_aware(datetime.utcnow())` to top * 1st round of changes Changes to be committed: modified: client/src/components/builder/index.js modified: client/src/components/builder/preview.js modified: client/src/components/viewer/cardViews.js modified: client/src/components/viewer/index.js modified: client/src/layouts/MainLayout/index.js * GA4 added * Add resetToken API for server For https://github.com/biocompute-objects/bco_api/issues/158 Changes to be committed: modified: server/bcodb/apis.py modified: server/bcodb/services.py modified: server/bcodb/urls.py * Token reset button for the client Fix https://github.com/biocompute-objects/bco_api/issues/158 * Add functions for ORCID authentication Changes to be committed: modified: server/authentication/apis.py modified: server/authentication/services.py modified: server/authentication/urls.py modified: server/bcodb/services.py modified: server/users/apis.py * Add/Remove ORCID for UI Fix #90 Changes to be committed: modified: client/src/components/account/Profile.js modified: client/src/services/auth.service.js modified: client/src/slices/accountSlice.js * Add/Remove ORCID for server Fix #90 Changes to be committed: modified: server/authentication/apis.py modified: server/authentication/urls.py modified: server/bcodb/services.py --------- Co-authored-by: tianywan819 <57418894+tianywan819@users.noreply.github.com> * Icons portalupdate (#107) * GA4 added * For demostration use, updated GA4, updated Navigation header icons --------- Co-authored-by: tianywan819 <57418894+tianywan819@users.noreply.github.com> * Derive from Published Fix #118 Changes to be committed: modified: client/src/components/viewer/index.js modified: client/src/slices/bcoSlice.js * Implement Derive From Function Builder Fix #117 Changes to be committed: modified: client/src/components/builder/index.js modified: client/src/components/builder/provenanceDomain.js modified: client/src/components/viewer/index.js --------- Co-authored-by: tianywan819 <57418894+tianywan819@users.noreply.github.com> --- client/src/App.js | 9 +- client/src/components/account/Profile.js | 59 +++++++++- client/src/components/account/Servers.js | 17 ++- client/src/components/builder/index.js | 21 +++- .../components/builder/provenanceDomain.js | 2 + client/src/components/viewer/index.js | 32 +++++- client/src/images/DBicon.svg | 7 ++ client/src/layouts/MainLayout/NavBar.js | 45 ++++---- client/src/services/auth.service.js | 72 +++++++++--- client/src/slices/accountSlice.js | 80 ++++++++++++- client/src/slices/bcoSlice.js | 28 +++++ server/authentication/apis.py | 106 +++++++++++++++++- server/authentication/services.py | 9 +- server/authentication/urls.py | 5 +- server/bcodb/apis.py | 41 ++++++- server/bcodb/services.py | 26 ++++- server/bcodb/urls.py | 3 +- server/users/apis.py | 5 - 18 files changed, 495 insertions(+), 72 deletions(-) create mode 100644 client/src/images/DBicon.svg diff --git a/client/src/App.js b/client/src/App.js index a794332e..1b42c86f 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -3,11 +3,12 @@ import React from "react"; import "./App.css"; import { BrowserRouter } from "react-router-dom"; import Router from "./routes"; -import TagManager from "react-gtm-module"; +import ReactGA from "react-ga4"; -function initializeReactGA() { - TagManager.initialize({gtmId:'GTM-TD6W6Q2'}); - } +function initializeReactGA(){ + ReactGA.initialize("G-RP8P4D7VWX"); + ReactGA.send({ hitType: "pageview", page: window.location.pathname }); +} function App() { initializeReactGA(); diff --git a/client/src/components/account/Profile.js b/client/src/components/account/Profile.js index d33b708e..2771d854 100644 --- a/client/src/components/account/Profile.js +++ b/client/src/components/account/Profile.js @@ -1,20 +1,43 @@ -import React from "react"; -import { Navigate } from "react-router-dom"; +import React, { useEffect} from "react"; +import { Navigate, useNavigate } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; -import { Button, Card, CardContent, CardHeader, Grid } from "@material-ui/core"; +import { Button, Card, CardContent, CardHeader, Grid, Typography } from "@material-ui/core"; import { Formik, Form, } from "formik"; import { MyTextField } from "../builder/specialFeilds"; import { account } from "../../slices/accountSlice"; import * as Yup from "yup"; +import { useSearchParams } from "react-router-dom"; +import { orcidAdd, orcidRemove } from "../../slices/accountSlice"; const Profile = () => { + const navigate = useNavigate(); const dispatch = useDispatch() const currentUser = useSelector((state) => state.account.user); + const orcidUrl = process.env.REACT_APP_ORCID_URL + const orcid_id = process.env.REACT_APP_ORCID_CLIENT_ID + const serverUrl = process.env.REACT_APP_SERVER_URL + const [searchParams, setSearchParams] = useSearchParams(); + const code = searchParams.get("code") if (!currentUser) { return ; } + useEffect(() => { + if (code !== null) { + console.log("response", code); + dispatch(orcidAdd(code)) + .unwrap() + .then(() => { + navigate("/profile"); + }) + .catch((error) => { + console.log(error) + navigate("/profile"); + }) + } + }, []) + return ( @@ -26,6 +49,7 @@ const Profile = () => { justifyContent="center" > { - + { (values.orcid.length > 3) + ? ( + + + ) + : ( + + ) + }
diff --git a/client/src/components/account/Servers.js b/client/src/components/account/Servers.js index d8a10ac2..42b48fc1 100644 --- a/client/src/components/account/Servers.js +++ b/client/src/components/account/Servers.js @@ -5,7 +5,7 @@ import { Button, Card, CardContent, CardHeader, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Grid, makeStyles, TextField, Typography } from "@material-ui/core" import { useSelector, useDispatch } from "react-redux"; -import { removeBcoDb, groupsPermissions, groupInfo } from "../../slices/accountSlice"; +import { removeBcoDb, resetToken } from "../../slices/accountSlice"; import AddServer from "./AddServer"; import { useNavigate } from "react-router-dom"; @@ -64,6 +64,16 @@ export default function Servers() { setOpen(false); }; + const handleTokenReset = (index) => { + const { public_hostname, token } = bcodbs[index] + console.log("Dispatch", public_hostname, token) + dispatch(resetToken({public_hostname, token})) + .unwrap() + .catch((error) =>{ + console.log(error); + }) + } + return ( BCO databases @@ -151,6 +161,11 @@ export default function Servers() { >Cancel + )) diff --git a/client/src/components/builder/index.js b/client/src/components/builder/index.js index b9c519de..8b4cbd7e 100644 --- a/client/src/components/builder/index.js +++ b/client/src/components/builder/index.js @@ -38,6 +38,7 @@ import { getDraftBco, publishDraftBco, validateBco, + deriveBco, setPrefix, updateETag, } from "../../slices/bcoSlice"; @@ -206,7 +207,6 @@ export const BuilderColorCode = () => { if (validURL(bco["object_id"]) === true) { navigate(`/builder?${bco["object_id"]}`); } - console.log("ELSE") }, [bco]) useEffect(()=> { @@ -219,8 +219,8 @@ export const BuilderColorCode = () => { .then(() => { console.log(bcoStatus) }) - .catch(() => { - console.log("Error"); + .catch((error) => { + console.log("Error", error); }); } @@ -235,6 +235,11 @@ export const BuilderColorCode = () => { setValue(value+1) } + const handleDerive = (jsonData) => { + navigate("/builder") + dispatch(deriveBco(jsonData)) + }; + function a11yProps(index) { return { id: `simple-tab-${index}`, @@ -423,6 +428,16 @@ export const BuilderColorCode = () => { +
+ + +
diff --git a/client/src/components/builder/provenanceDomain.js b/client/src/components/builder/provenanceDomain.js index 9a39728a..77ed9d52 100644 --- a/client/src/components/builder/provenanceDomain.js +++ b/client/src/components/builder/provenanceDomain.js @@ -15,6 +15,7 @@ export const ProvenanceDomain = ({onSave} ) => { let has_obsolete = "obsolete_after" in provenanceDomain; let has_embargo = "embargo" in provenanceDomain; let has_review = "review" in provenanceDomain; + let is_derived = "derived_from" in provenanceDomain; const [obsolete, setObsolete] = useState("obsolete_after" in provenanceDomain) const [embargo, setEmbargo] = useState("embargo" in provenanceDomain) return ( @@ -33,6 +34,7 @@ export const ProvenanceDomain = ({onSave} ) => { "license": provenanceDomain["license"], "created": provenanceDomain["created"], "modified": provenanceDomain["modified"], + "derived_from": is_derived ? provenanceDomain["derived_from"] : [], "obsolete_after": has_obsolete ? provenanceDomain["obsolete_after"] : [], "contributors": provenanceDomain["contributors"], "review": has_review ? provenanceDomain["review"] : [], diff --git a/client/src/components/viewer/index.js b/client/src/components/viewer/index.js index e7fb7a2e..429ec102 100644 --- a/client/src/components/viewer/index.js +++ b/client/src/components/viewer/index.js @@ -2,9 +2,19 @@ import React, { useEffect, useState } from "react"; import { - Button, Card, CardActions, CardHeader, CardContent, Collapse, Container, Grid, ListItem, ListItemText, Paper, + Button, + Card, + CardActions, + CardHeader, + CardContent, + Collapse, + Container, + Grid, + ListItem, + ListItemText, Typography } from "@material-ui/core"; +import { useNavigate } from "react-router-dom"; import DataObjectIcon from "@mui/icons-material/DataObject"; import { useDispatch, useSelector } from "react-redux"; import PropTypes from "prop-types"; @@ -12,9 +22,9 @@ import { ProvenanceView, UsabilityView, DescriptionView, ExtensionView, ExecutionView, ParametricView, IoView, ErrorView } from "./cardViews"; -import { FileUpload, handleDownloadClick } from "../fileHandeling"; +import { handleDownloadClick } from "../fileHandeling"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { getPubBco } from "../../slices/bcoSlice"; +import { getPubBco, deriveBco } from "../../slices/bcoSlice"; import { styled } from "@mui/material/styles"; import IconButton from "@mui/material/IconButton"; @@ -65,6 +75,7 @@ const ExpandMore = styled((props) => { })); export default function BcoViewer () { + const navigate = useNavigate(); const dispatch = useDispatch(); const [value, setValue] = React.useState(0); const bco = useSelector(state => state.bco.data) @@ -79,6 +90,11 @@ export default function BcoViewer () { setValue(newValue); }; + const handleDerive = (jsonData) => { + navigate("/builder") + dispatch(deriveBco(jsonData)) + }; + function a11yProps(index) { return { id: `simple-tab-${index}`, @@ -167,6 +183,16 @@ export default function BcoViewer () { onClick={() => {handleDownloadClick(jsonData)}} > Download BCO +
+ + +
diff --git a/client/src/images/DBicon.svg b/client/src/images/DBicon.svg new file mode 100644 index 00000000..ae71893a --- /dev/null +++ b/client/src/images/DBicon.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/client/src/layouts/MainLayout/NavBar.js b/client/src/layouts/MainLayout/NavBar.js index 4bb472ae..7169b3b3 100644 --- a/client/src/layouts/MainLayout/NavBar.js +++ b/client/src/layouts/MainLayout/NavBar.js @@ -12,17 +12,22 @@ import Menu from "@material-ui/core/Menu"; import Tooltip, { tooltipClasses } from "@mui/material/Tooltip"; import LoginIcon from '@mui/icons-material/Login'; import AccountCircle from "@material-ui/icons/AccountCircle"; -import ConstructionIcon from "@mui/icons-material/Construction"; -import DataObjectIcon from "@mui/icons-material/DataObject"; +// import EditOutlinedIcon from "@mui/icons-material/Construction"; +// import SearchIcon from "@mui/icons-material/DataObject"; import BugReportIcon from "@mui/icons-material/BugReport"; -import AppRegistrationIcon from "@mui/icons-material/AppRegistration"; +// import GroupIcon from "@mui/icons-material/AppRegistration"; import { useSelector } from "react-redux"; import MoreIcon from "@material-ui/icons/MoreVert"; import { Link } from "react-router-dom"; -import MiscellaneousServicesIcon from "@mui/icons-material/MiscellaneousServices"; +// import SettingsInputAntennaIcon from "@mui/icons-material/MiscellaneousServices"; import ContactPageIcon from "@mui/icons-material/ContactPage"; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { Info } from "@mui/icons-material"; +import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; +// import SearchIcon from '@mui/icons-material/Storage'; +import SearchIcon from '@mui/icons-material/Search'; +import GroupIcon from '@mui/icons-material/Group'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; const useStyles = makeStyles((theme) => ({ grow: { @@ -103,7 +108,7 @@ const NavBar = () => { - +

BCO Resources

@@ -111,7 +116,7 @@ const NavBar = () => { - +

BCO Builder

@@ -119,7 +124,7 @@ const NavBar = () => { - +

Prefix Registry

@@ -127,7 +132,7 @@ const NavBar = () => { - +

BCO DB

@@ -196,7 +201,7 @@ const NavBar = () => { - +

BCO Resources

@@ -204,7 +209,7 @@ const NavBar = () => { - +

BCO Builder

@@ -212,7 +217,7 @@ const NavBar = () => { - +

Prefix Registry

@@ -220,7 +225,7 @@ const NavBar = () => { - +

BCO DB

@@ -285,28 +290,28 @@ const NavBar = () => { - + - + - + - + @@ -363,28 +368,28 @@ const NavBar = () => { - + - + - + - + diff --git a/client/src/services/auth.service.js b/client/src/services/auth.service.js index 3c957ce0..a1f3f834 100644 --- a/client/src/services/auth.service.js +++ b/client/src/services/auth.service.js @@ -58,6 +58,33 @@ const orcidLogIn = async (code) => { return response.data; } +const orcidAdd = async (code) => { + const response = await axios.post(`${USERS_URL}orcid/add/?code=${code}`, {}, + { + headers: { + "Authorization": `Bearer ${JSON.parse(localStorage.getItem("token"))}`, + "Content-Type": "application/json" + }}) + if (response.data.token) { + localStorage.setItem("user", JSON.stringify(response.data.user)); + localStorage.setItem("token", JSON.stringify(response.data.token)); + } + return response.data; +} + +const orcidRemove = async () => { + const response = await axios.post(`${USERS_URL}orcid/remove/`, {},{ + headers: { + "Authorization": `Bearer ${JSON.parse(localStorage.getItem("token"))}`, + "Content-Type": "application/json" + }}) + if (response.data.token) { + localStorage.setItem("user", JSON.stringify(response.data.user)); + localStorage.setItem("token", JSON.stringify(response.data.token)); + } + return response.data; +} + const logout = () => { localStorage.removeItem("user"); localStorage.removeItem("token"); @@ -66,20 +93,21 @@ const logout = () => { const account = async (data) => { const response = await axios - .post(USERS_URL + "update_user/", { - "username": data.username, - "first_name": data.first_name, - "last_name": data.last_name, - "email": data.email, - "affiliation": data.affiliation, - "orcid": data.orcid, - "public": data.public, - }, { - headers: { - "Authorization": `Bearer ${JSON.parse(localStorage.getItem("token"))}`, - "Content-Type": "application/json" - } - }); + .post(USERS_URL + "update_user/", + { + "username": data.username, + "first_name": data.first_name, + "last_name": data.last_name, + "email": data.email, + "affiliation": data.affiliation, + "orcid": data.orcid, + "public": data.public, + }, { + headers: { + "Authorization": `Bearer ${JSON.parse(localStorage.getItem("token"))}`, + "Content-Type": "application/json" + } + }); if (response.data.token) { localStorage.setItem("user", JSON.stringify(response.data.user)); } @@ -210,6 +238,19 @@ const removeBcoDb = async (database) => { return response; } +const resetToken = async (public_hostname, token) => { + console.log("Service", public_hostname, token) + const response = await axios.post(`${USERS_URL}bcodb/reset_token/`, { + public_hostname, token + },{ + headers: { + "Authorization": `Bearer ${JSON.parse(localStorage.getItem("token"))}`, + "Content-Type": "application/json" + } + }); + return response; +} + const groupInfo = async (group_permissions, token, public_hostname) => { const response = await axios.post(`${public_hostname}/api/groups/group_info/`, { POST_api_groups_info: { @@ -236,6 +277,8 @@ const authService = { googleRegister, userInfo, orcidLogIn, + orcidAdd, + orcidRemove, advSearchBcodbAPI, searchBcodbAPI, authenticateBcoDb, @@ -243,6 +286,7 @@ const authService = { addBcoDb, removeBcoDb, groupInfo, + resetToken, }; export default authService; \ No newline at end of file diff --git a/client/src/slices/accountSlice.js b/client/src/slices/accountSlice.js index a67ed069..86f53a92 100644 --- a/client/src/slices/accountSlice.js +++ b/client/src/slices/accountSlice.js @@ -178,7 +178,6 @@ export const orcidLogIn = createAsyncThunk( try { const authentication = await AuthService.orcidLogIn(code); // thunkAPI.dispatch(setMessage(authentication.data.message)); - console.log(authentication) return authentication } catch (error) { const message = @@ -193,6 +192,46 @@ export const orcidLogIn = createAsyncThunk( } ) +export const orcidAdd = createAsyncThunk( + "auth/orcidAdd", + async (code, thunkAPI) => { + try { + const authentication = await AuthService.orcidAdd(code); + thunkAPI.dispatch(setMessage("ORCID added to user profile")); + return authentication + } catch (error) { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + thunkAPI.dispatch(setMessage(message)); + return thunkAPI.rejectWithValue(); + } + } +); + +export const orcidRemove = createAsyncThunk( + "auth/orcidRemove", + async (thunkAPI) => { + try { + const remove = await AuthService.orcidRemove(); + return remove + } catch (error) { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + thunkAPI.dispatch(setMessage(message)); + return thunkAPI.rejectWithValue(); + } + + } +); + export const login = createAsyncThunk( "auth/login", async ({ username, password }, thunkAPI) => { @@ -221,11 +260,9 @@ export const logout = createAsyncThunk("auth/logout", async (thunkAPI) => { export const authenticateBcoDb = createAsyncThunk( "bcodb/addServer", async ({ token, hostname }, thunkAPI) => { - console.log(token, hostname); try { const bcodbResponse = await AuthService.authenticateBcoDb(token, hostname); const userDbResponse = await AuthService.addBcoDb(bcodbResponse) - console.log(userDbResponse) thunkAPI.dispatch(setMessage(userDbResponse.data.message)); return userDbResponse.data ; } catch (error) { @@ -261,6 +298,26 @@ export const removeBcoDb = createAsyncThunk( } ); +export const resetToken = createAsyncThunk( + "bcodb/resetToken", + async ({ public_hostname, token }, thunkAPI) => { + try { + const response = await AuthService.resetToken(public_hostname, token); + thunkAPI.dispatch(setMessage("Token reset successfull.")); + return response.data + } catch (error) { + const message = + (error.response && + error.response.data && + error.response.data.message) || + error.message || + error.toString(); + thunkAPI.dispatch(setMessage(message)); + return thunkAPI.rejectWithValue(); + } + } +); + export const groupInfo = createAsyncThunk( "bcodb/groupInfo", async ({group_permissions, token, public_hostname, index}, thunkAPI) => { @@ -330,6 +387,20 @@ export const accountSlice = createSlice({ state.isLoggedIn = false; state.user = null; }) + .addCase(orcidAdd.fulfilled, (state, action) => { + state.user = action.payload.user; + }) + .addCase(orcidAdd.rejected, (state, action) => { + console.log(action.payload) + // state.user = action.payload.user; + }) + .addCase(orcidRemove.fulfilled, (state, action) => { + state.user = action.payload.user; + }) + .addCase(orcidRemove.rejected, (state, action) => { + console.log(action.payload) + // state.user = action.payload.user; + }) .addCase(googleLogin.fulfilled, (state, action) => { state.isLoggedIn = true; state.user = action.payload.user; @@ -367,6 +438,9 @@ export const accountSlice = createSlice({ .addCase(groupInfo.fulfilled, (state, action) => { state.user.bcodbs[action.payload[1]].groups_info = action.payload[0] }) + .addCase(resetToken.fulfilled, (state, action) => { + state.user = action.payload.user; + }) }, }); diff --git a/client/src/slices/bcoSlice.js b/client/src/slices/bcoSlice.js index d960179f..a16af316 100644 --- a/client/src/slices/bcoSlice.js +++ b/client/src/slices/bcoSlice.js @@ -81,6 +81,33 @@ const bcoSlice = createSlice({ }, updateBco: (state, action) => { state["data"] = action.payload + }, + deriveBco: (state, action) => { + const derive = action.payload + state["data"] = { + object_id: "", + spec_version: "https://w3id.org/ieee/ieee-2791-schema/2791object.json", + etag: "", + provenance_domain: { + name: derive.provenance_domain.name, + version: "", + license: "", + derived_from: derive.object_id, + created: new Date().toISOString().split(".")[0], + modified: new Date().toISOString(), + contributors: derive.provenance_domain.contributors, + review: derive.provenance_domain.review + }, + usability_domain: derive.usability_domain, + description_domain: derive.description_domain, + parametric_domain:derive.parametric_domain, + io_domain: derive.io_domain, + execution_domain: derive.execution_domain, + extension_domain: derive.extension_domain, + }, + state["prefix"] = null, + state["status"] = "idle", + state["error"] = null } }, extraReducers(builder) { @@ -320,4 +347,5 @@ export const { setPrefix, updateETag, updateBco, + deriveBco, } = bcoSlice.actions; \ No newline at end of file diff --git a/server/authentication/apis.py b/server/authentication/apis.py index d864ef45..07f39f31 100644 --- a/server/authentication/apis.py +++ b/server/authentication/apis.py @@ -4,12 +4,15 @@ """ import jwt +from bcodb.services import add_authentication, remove_authentication +from bcodb.selectors import get_all_bcodbs from django.conf import settings from django.contrib.auth.models import User from django.core.mail import send_mail from django.dispatch import receiver from django.urls import reverse from django_rest_passwordreset.signals import reset_password_token_created +from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import permissions, status, serializers, exceptions from rest_framework.response import Response @@ -18,7 +21,7 @@ from authentication.services import custom_jwt_handler, google_authentication, orcid_auth_code, authenticate_orcid from users.models import Profile from users.services import user_create -from users.selectors import user_from_email, user_from_orcid +from users.selectors import user_from_email, user_from_orcid, profile_from_username @receiver(reset_password_token_created) def password_reset_token_created( @@ -77,8 +80,15 @@ class OrcidUserInfoApi(APIView): authentication_classes = [] permission_classes = [] - + authentication = [ + openapi.Parameter( + "Authorization", + openapi.IN_HEADER, + description="Authorization Token", + type=openapi.TYPE_STRING, + )] @swagger_auto_schema( + manual_parameters=authentication, responses={ 200: "Request is successful.", 401: "A user with that ORCID does not exist.", @@ -88,7 +98,11 @@ class OrcidUserInfoApi(APIView): ) def post(self, request): - """ + """Orcid User Info Api + ---------------------- + No schema for this request since only the Authorization header is required. + The word 'Bearer' must be included in the header. + For example: 'Bearer #####################################################' """ if 'Authorization' in request.headers: type, token = request.headers['Authorization'].split(' ') @@ -135,7 +149,7 @@ def get(self, request): """ auth_code = self.request.GET['code'] - orcid_auth = orcid_auth_code(auth_code) + orcid_auth = orcid_auth_code(auth_code, path='/login') if "access_token" not in orcid_auth: return Response(status=status.HTTP_401_UNAUTHORIZED, data={"message": orcid_auth['error_description']}) user = user_from_orcid(orcid_auth['orcid']) @@ -155,6 +169,90 @@ def get(self, request): data={"message": "That account does not exist"}, ) +class OrcidAddApi(APIView): + """Add Orcid API + This API view allows for a user to add an ORCID for OAuth authentication. The + request should include a valid JWT token in the authorization header. + + Returns the updated user information in the response body. + """ + + + @swagger_auto_schema( + responses={ + 200: "Add ORCID successful.", + 401: "Unathorized.", + 500: "Error" + }, + tags=["Account Management"], + ) + def post(self, request): + auth_code = self.request.GET['code'] + orcid_auth = orcid_auth_code(auth_code, path='/profile') + if "access_token" not in orcid_auth: + return Response( + status=status.HTTP_401_UNAUTHORIZED, + data={"message": orcid_auth['error_description']} + ) + + conflict = user_from_orcid(orcid_auth['orcid']) + if conflict != 0: + return Response( + status=status.HTTP_403_FORBIDDEN, + data={"message": "A user with that ORCID already exists"}, + ) + + user = request.user + token = request.headers["Authorization"].removeprefix("Bearer ") + profile = profile_from_username(user.username) + bcodbs = get_all_bcodbs(profile) + profile.orcid = settings.ORCID_URL + '/' + orcid_auth['orcid'] + profile.save() + auth_obj = { + "iss": settings.ORCID_URL, + "sub": orcid_auth['orcid'] + } + for bcodb in bcodbs: + add_authentication(auth_obj, bcodb) + return Response( + status=status.HTTP_200_OK, data=custom_jwt_handler(token, user) + ) + +class OrcidRemoveApi(APIView): + """Remove Orcid API + This API view allows for a user to remove an ORCID for OAuth authentication. The + request should include a valid JWT token in the authorization header. + + Returns the updated user information in the response body. + """ + + @swagger_auto_schema( + responses={ + 200: "Add ORCID successful.", + 401: "Unathorized.", + 500: "Error" + }, + tags=["Account Management"], + ) + def post(self, request): + """""" + user = request.user + profile = profile_from_username(user.username) + token = request.headers["Authorization"].removeprefix("Bearer ") + auth_obj = { + "iss": settings.ORCID_URL, + "sub": profile.orcid.split('/')[-1] + } + profile.orcid = "" + profile.save() + bcodbs = get_all_bcodbs(profile) + for bcodb in bcodbs: + remove_authentication(auth_obj, bcodb) + return Response( + status=status.HTTP_200_OK, data=custom_jwt_handler(token, user) + ) + + class GoogleUsername(serializers.CharField): """ Custom serializer field for Google username that removes whitespace from diff --git a/server/authentication/services.py b/server/authentication/services.py index a3aa1fe7..9312159a 100644 --- a/server/authentication/services.py +++ b/server/authentication/services.py @@ -43,15 +43,18 @@ def authenticate_orcid(iss_oauth, token): # return user -def orcid_auth_code(code: str)-> Response: - """ +def orcid_auth_code(code: str, path: str)-> Response: + """ORCID Authorization Code + + Verifies the ORCID authentication. """ + data = { "client_id": settings.ORCID_CLIENT, "client_secret": settings.ORCID_SECRET, "grant_type": "authorization_code", "code": code, - "redirect_uri": settings.CLIENT + "/login" + "redirect_uri": settings.CLIENT + path } headers = { "Accept": "application/json", diff --git a/server/authentication/urls.py b/server/authentication/urls.py index 81a2274a..c14e9b6d 100644 --- a/server/authentication/urls.py +++ b/server/authentication/urls.py @@ -4,7 +4,8 @@ refresh_jwt_token, verify_jwt_token, ) -from authentication.apis import GoogleLoginApi, GoogleRegisterApi, OrcidLoginApi, OrcidUserInfoApi + +from authentication.apis import GoogleLoginApi, GoogleRegisterApi, OrcidLoginApi, OrcidUserInfoApi, OrcidAddApi, OrcidRemoveApi from users.apis import UserCreateApi urlpatterns = [ @@ -16,6 +17,8 @@ path("google/register/", GoogleRegisterApi.as_view()), path("orcid/login/", OrcidLoginApi.as_view()), path("orcid/user_info/", OrcidUserInfoApi.as_view()), + path("orcid/add/", OrcidAddApi.as_view()), + path("orcid/remove/", OrcidRemoveApi.as_view()), # path("orcid/register/", GoogleRegisterApi.as_view()), path("password_reset/", include('django_rest_passwordreset.urls', namespace='password_reset')), ] diff --git a/server/bcodb/apis.py b/server/bcodb/apis.py index 668273fa..135b5c1f 100644 --- a/server/bcodb/apis.py +++ b/server/bcodb/apis.py @@ -4,6 +4,7 @@ """ from django.db import transaction +from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.views import APIView @@ -12,7 +13,7 @@ from datetime import datetime from users.selectors import profile_from_username, user_from_username from authentication.services import custom_jwt_handler -from bcodb.services import create_bcodb +from bcodb.services import create_bcodb, reset_token from bcodb.selectors import get_bcodb @@ -98,4 +99,42 @@ def post(self, request): request._auth, user_from_username(request.user.username) ) + return Response(status=status.HTTP_200_OK, data=user_info) + +class ResetBcodbTokenApi(APIView): + """Remove a BCODB from a user account""" + schema = openapi.Schema( + type=openapi.TYPE_OBJECT, + title="Reset Token", + description="Will reset the BCODB token", + required=["token", "public_hostname"], + properties={ + "token": openapi.Schema( + type=openapi.TYPE_STRING, + description="The BCODB token to be reset." + ), + "public_hostname": openapi.Schema( + type=openapi.TYPE_STRING, + description="The BCODB public hostname (URL)." + ) + } + ) + + @swagger_auto_schema( + request_body=schema, + responses={ + 200: "BCODB token reset is successful.", + 409: "Conflict.", + }, + tags=["BCODB Management"], + ) + + # @transaction.atomic + def post(self, request): + """""" + public_hostname, token = request.data['public_hostname'], request.data['token'] + reset_token(public_hostname, token) + user_info = custom_jwt_handler( + request._auth, user_from_username(request.user.username) + ) return Response(status=status.HTTP_200_OK, data=user_info) \ No newline at end of file diff --git a/server/bcodb/services.py b/server/bcodb/services.py index 89f549eb..70d0f3d0 100644 --- a/server/bcodb/services.py +++ b/server/bcodb/services.py @@ -70,7 +70,7 @@ def create_bcodb(data: dict) -> BcoDb: return bcodb_serializer.data -def add_authentication(token: str, auth_object: dict, bcodb: BcoDb): +def add_authentication(auth_object: dict, bcodb: BcoDb): """Add Authentication Adds an authentication object to the BCODB object. """ @@ -79,7 +79,7 @@ def add_authentication(token: str, auth_object: dict, bcodb: BcoDb): url=bcodb.public_hostname + "/api/auth/add/", data=json.dumps(auth_object), headers= { - "Authorization": "Bearer " + token, + "Authorization": "Token " + bcodb.token, "Content-type": "application/json; charset=UTF-8", } ) @@ -88,7 +88,7 @@ def add_authentication(token: str, auth_object: dict, bcodb: BcoDb): except Exception as err: print(err) -def remove_authentication(token: str, auth_object: dict, bcodb: BcoDb): +def remove_authentication(auth_object: dict, bcodb: BcoDb): """Remove Authentication Removes an authentication object to the BCODB object. """ @@ -97,7 +97,7 @@ def remove_authentication(token: str, auth_object: dict, bcodb: BcoDb): url=bcodb.public_hostname + "/api/auth/remove/", data=json.dumps(auth_object), headers= { - "Authorization": "Bearer " + token, + "Authorization": "Token " + bcodb.token, "Content-type": "application/json; charset=UTF-8", } ) @@ -106,4 +106,20 @@ def remove_authentication(token: str, auth_object: dict, bcodb: BcoDb): except Exception as err: print(err) - +def reset_token(public_hostname: str, token: str) -> BcoDb: + """Reset BCODB Token""" + + try: + bco_api_response = requests.post( + url=public_hostname + "/api/auth/reset_token/", + data={}, + headers= { + "Authorization": "Token " + token, + "Content-type": "application/json; charset=UTF-8", + }, + ) + BcoDb.objects.filter(token=token).update(token=bco_api_response.json()['token']) + + return bco_api_response.json() + except: + return bco_api_response.json() \ No newline at end of file diff --git a/server/bcodb/urls.py b/server/bcodb/urls.py index 6f4d4265..3c5d7c7b 100644 --- a/server/bcodb/urls.py +++ b/server/bcodb/urls.py @@ -2,10 +2,11 @@ from django.urls import path from rest_framework_jwt.views import obtain_jwt_token -from bcodb.apis import getRouts, AddBcodbApi, RemoveBcodbApi # getBcoDb +from bcodb.apis import getRouts, AddBcodbApi, RemoveBcodbApi, ResetBcodbTokenApi # getBcoDb urlpatterns = [ path("", getRouts), path("bcodb/add/", AddBcodbApi.as_view()), path("bcodb/remove/", RemoveBcodbApi.as_view()), + path("bcodb/reset_token/", ResetBcodbTokenApi.as_view()), ] diff --git a/server/users/apis.py b/server/users/apis.py index 660331dc..cb38d59c 100644 --- a/server/users/apis.py +++ b/server/users/apis.py @@ -164,11 +164,6 @@ def post(self, request): if profile.orcid != data["orcid"]: bcodbs = get_all_bcodbs(profile) if data['orcid'] == '': - auth_obj = { - "iss": "https://" + profile.orcid.split("/")[-2], - "sub": profile.orcid.split("/")[-1] - } - print('Remove') for bcodb in bcodbs: remove_authentication(token, auth_obj, bcodb) else: