From 62ad93eae3fb8be7357ce3748e95737db6962a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6tzsch?= Date: Wed, 3 May 2023 15:15:15 +0200 Subject: [PATCH] password reset --- backend/src/swlkup/resolver/core.clj | 4 +- .../root/ngo/supervisor_password_reset.clj | 37 +++++++++++++++ backend/test/swlkup/introspection_test.clj | 2 +- frontend/codegen/generates.ts | 30 +++++++++---- .../components/ngo/InvalidateTokenDialog.tsx | 2 +- frontend/components/ngo/InviteSupervisor.tsx | 24 +++++++--- .../ngo/SupervisorPasswordReset.tsx | 45 +++++++++++++++++++ frontend/pages/supervisor/edit.tsx | 8 +++- 8 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 backend/src/swlkup/resolver/root/ngo/supervisor_password_reset.clj create mode 100644 frontend/components/ngo/SupervisorPasswordReset.tsx diff --git a/backend/src/swlkup/resolver/core.clj b/backend/src/swlkup/resolver/core.clj index ec52d12..3d89ff8 100644 --- a/backend/src/swlkup/resolver/core.clj +++ b/backend/src/swlkup/resolver/core.clj @@ -22,6 +22,7 @@ [swlkup.resolver.root.ngo.create-token :refer [create_token]] [swlkup.resolver.root.ngo.created-tokens :refer [created_tokens]] [swlkup.resolver.root.ngo.invalidate-token :refer [invalidate_token]] + [swlkup.resolver.root.ngo.supervisor-password-reset :refer [supervisor_password_reset]] ;; admin passphrase [swlkup.resolver.root.admin.export :refer [export]])) @@ -39,7 +40,8 @@ :supervisor_delete #'supervisor_delete :supervisor_deactivate #'supervisor_deactivate :create_token #'create_token - :invalidate_token #'invalidate_token}})) + :invalidate_token #'invalidate_token + :supervisor_password_reset #'supervisor_password_reset}})) (defn ->graphql "Create a wrapped graphql-executor, that merges context into the request. diff --git a/backend/src/swlkup/resolver/root/ngo/supervisor_password_reset.clj b/backend/src/swlkup/resolver/root/ngo/supervisor_password_reset.clj new file mode 100644 index 0000000..6a626e7 --- /dev/null +++ b/backend/src/swlkup/resolver/root/ngo/supervisor_password_reset.clj @@ -0,0 +1,37 @@ +(ns swlkup.resolver.root.ngo.supervisor-password-reset + (:require [clojure.spec.alpha :as s] + [specialist-server.type :as t] + [swlkup.auth.core :refer [auth+role->entity]] + [swlkup.model.auth :as auth] + [swlkup.model.ngo :as ngo] + [swlkup.model.login :as login] + [swlkup.auth.password.generate :refer [generate-password]] + [swlkup.auth.password.hash :refer [hash-password]])) + +(s/def ::supervisor t/string) + +(s/fdef supervisor_password_reset + :args (s/tuple map? (s/keys :req-un [::auth/auth ::supervisor]) map? map?) + :ret (s/keys :req-un [::login/mail ::login/password])) + +(defn supervisor_password_reset + [_node opt ctx _info] + (let [{:keys [q_id_unary tx_sync, tx-committed?]} (:db_ctx ctx) + [ngo:id] (auth+role->entity ctx (:auth opt) ::ngo/doc) + supervisor (:supervisor opt)] + (when ngo:id + (let [login (q_id_unary '{:find [(pull ?l [*])] + :where [[?l :xt/spec ::login/doc] + [?l :invited-by ?ngo:id] + [?l :mail ?mail]] + :in [[?ngo:id ?mail]]} + [ngo:id supervisor]) + password (generate-password) + password-hash (hash-password password) + t (when login + (tx_sync [[:xtdb.api/put (assoc login + :password-hash password-hash)]]))] + (when (tx-committed? t) + {:mail (:mail login) :password password}))))) + +(s/def ::supervisor_password_reset (t/resolver #'supervisor_password_reset)) diff --git a/backend/test/swlkup/introspection_test.clj b/backend/test/swlkup/introspection_test.clj index e453717..d4768c4 100644 --- a/backend/test/swlkup/introspection_test.clj +++ b/backend/test/swlkup/introspection_test.clj @@ -10,4 +10,4 @@ (get-in [:data :__schema :types])) (map :name) sort) - '("Auth" "Boolean" "Contacts" "ContactsInput" "Float" "ID" "Int" "Location" "LocationInput" "Long" "MutationType" "NgoRefs" "QueryType" "String" "SupervisorInput" "created_tokens" "export" "languages" "login" "lookup" "ngo" "ngos" "offers" "supervisor_get" "supervisors" "supervisors_registered")))) + '("Auth" "Boolean" "Contacts" "ContactsInput" "Float" "ID" "Int" "Location" "LocationInput" "Long" "MutationType" "NgoRefs" "QueryType" "String" "SupervisorInput" "created_tokens" "export" "languages" "login" "lookup" "ngo" "ngos" "offers" "supervisor_get" "supervisor_password_reset" "supervisors" "supervisors_registered")))) diff --git a/frontend/codegen/generates.ts b/frontend/codegen/generates.ts index 147298e..def886d 100644 --- a/frontend/codegen/generates.ts +++ b/frontend/codegen/generates.ts @@ -12,9 +12,7 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; - /** The 'Long' scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^64) and 2^64 - 1. */ Long: any; - /** Either a collection of ngo-ids or `any` */ NgoRefs: any; }; @@ -75,6 +73,7 @@ export type MutationType = { supervisor_deactivate: Scalars['Boolean']; /** Delete a supervisors dataset and login */ supervisor_delete: Scalars['Boolean']; + supervisor_password_reset: Supervisor_Password_Reset; /** Add a new supervisor account to the database and send a mail containing the password via mail */ supervisor_register: Scalars['Boolean']; /** @@ -116,6 +115,13 @@ export type MutationTypeSupervisor_DeleteArgs = { }; +/** If this server supports mutation, the type that mutation operations will be rooted at. */ +export type MutationTypeSupervisor_Password_ResetArgs = { + auth: Auth; + supervisor: Scalars['String']; +}; + + /** If this server supports mutation, the type that mutation operations will be rooted at. */ export type MutationTypeSupervisor_RegisterArgs = { auth: Auth; @@ -308,6 +314,14 @@ export type Supervisor_Get = { text_specialization?: Maybe; }; +export type Supervisor_Password_Reset = { + __typename?: 'supervisor_password_reset'; + /** Self descriptive. */ + mail: Scalars['String']; + /** Self descriptive. */ + password: Scalars['String']; +}; + /** All supervisor visible with the used credentials */ export type Supervisors = { __typename?: 'supervisors'; @@ -344,7 +358,7 @@ export type LoginQueryVariables = Exact<{ }>; -export type LoginQuery = { __typename?: 'QueryType', login: { __typename?: 'login', jwt?: string | null | undefined } }; +export type LoginQuery = { __typename?: 'QueryType', login: { __typename?: 'login', jwt?: string | null } }; export type LanguagesQueryVariables = Exact<{ [key: string]: never; }>; @@ -356,28 +370,28 @@ export type LookupQueryVariables = Exact<{ }>; -export type LookupQuery = { __typename?: 'QueryType', lookup: { __typename?: 'lookup', valid: boolean, ngo?: { __typename?: 'ngo', name?: string | null | undefined } | null | undefined, supervisors?: Array<{ __typename?: 'supervisors', id: string, name_full: string, photo?: string | null | undefined, languages: Array, offers: Array, text_specialization?: string | null | undefined, text?: string | null | undefined, contacts: { __typename?: 'Contacts', phone?: string | null | undefined, email?: string | null | undefined, website?: string | null | undefined }, location: { __typename?: 'Location', country?: string | null | undefined, city?: string | null | undefined, zip?: string | null | undefined, type?: string | null | undefined, importance?: number | null | undefined, display_name?: string | null | undefined, lat?: number | null | undefined, lon?: number | null | undefined, diameter?: number | null | undefined } }> | null | undefined }, languages: Array<{ __typename?: 'languages', id: string, name: string, flag_url: string, idx: number }>, offers: Array<{ __typename?: 'offers', id: string, target: string, idx: number }> }; +export type LookupQuery = { __typename?: 'QueryType', lookup: { __typename?: 'lookup', valid: boolean, ngo?: { __typename?: 'ngo', name?: string | null } | null, supervisors?: Array<{ __typename?: 'supervisors', id: string, name_full: string, photo?: string | null, languages: Array, offers: Array, text_specialization?: string | null, text?: string | null, contacts: { __typename?: 'Contacts', phone?: string | null, email?: string | null, website?: string | null }, location: { __typename?: 'Location', country?: string | null, city?: string | null, zip?: string | null, type?: string | null, importance?: number | null, display_name?: string | null, lat?: number | null, lon?: number | null, diameter?: number | null } }> | null }, languages: Array<{ __typename?: 'languages', id: string, name: string, flag_url: string, idx: number }>, offers: Array<{ __typename?: 'offers', id: string, target: string, idx: number }> }; export type SupervisorGetQueryVariables = Exact<{ auth: Auth; }>; -export type SupervisorGetQuery = { __typename?: 'QueryType', supervisor_get?: { __typename?: 'supervisor_get', id: string, deactivated?: boolean | null | undefined, ngos: any, name_full: string, languages: Array, offers: Array, text_specialization?: string | null | undefined, text?: string | null | undefined, ngo?: string | null | undefined, contacts: { __typename?: 'Contacts', phone?: string | null | undefined, website?: string | null | undefined, email?: string | null | undefined }, location: { __typename?: 'Location', country?: string | null | undefined, city?: string | null | undefined, zip?: string | null | undefined, type?: string | null | undefined, importance?: number | null | undefined, display_name?: string | null | undefined, lat?: number | null | undefined, lon?: number | null | undefined, diameter?: number | null | undefined } } | null | undefined, languages: Array<{ __typename?: 'languages', id: string, name: string, flag_url: string, idx: number }>, offers: Array<{ __typename?: 'offers', id: string, target: string, idx: number }>, ngos: Array<{ __typename?: 'ngos', id?: string | null | undefined, name?: string | null | undefined }> }; +export type SupervisorGetQuery = { __typename?: 'QueryType', supervisor_get?: { __typename?: 'supervisor_get', id: string, deactivated?: boolean | null, ngos: any, name_full: string, languages: Array, offers: Array, text_specialization?: string | null, text?: string | null, ngo?: string | null, contacts: { __typename?: 'Contacts', phone?: string | null, website?: string | null, email?: string | null }, location: { __typename?: 'Location', country?: string | null, city?: string | null, zip?: string | null, type?: string | null, importance?: number | null, display_name?: string | null, lat?: number | null, lon?: number | null, diameter?: number | null } } | null, languages: Array<{ __typename?: 'languages', id: string, name: string, flag_url: string, idx: number }>, offers: Array<{ __typename?: 'offers', id: string, target: string, idx: number }>, ngos: Array<{ __typename?: 'ngos', id?: string | null, name?: string | null }> }; export type SupervisorGetPhotoQueryVariables = Exact<{ auth: Auth; }>; -export type SupervisorGetPhotoQuery = { __typename?: 'QueryType', supervisor_get?: { __typename?: 'supervisor_get', photo?: string | null | undefined } | null | undefined }; +export type SupervisorGetPhotoQuery = { __typename?: 'QueryType', supervisor_get?: { __typename?: 'supervisor_get', photo?: string | null } | null }; export type NgoQueryVariables = Exact<{ auth: Auth; }>; -export type NgoQuery = { __typename?: 'QueryType', created_tokens: Array<{ __typename?: 'created_tokens', token: string, purpose?: string | null | undefined, valid: boolean }>, supervisors_registered: Array<{ __typename?: 'supervisors_registered', mail: string, name_full?: string | null | undefined }> }; +export type NgoQuery = { __typename?: 'QueryType', created_tokens: Array<{ __typename?: 'created_tokens', token: string, purpose?: string | null, valid: boolean }>, supervisors_registered: Array<{ __typename?: 'supervisors_registered', mail: string, name_full?: string | null }> }; export const LoginDocument = ` @@ -580,4 +594,4 @@ export const useNgoQuery = < ['Ngo', variables], fetcher(NgoDocument, variables), options - ); \ No newline at end of file + ); diff --git a/frontend/components/ngo/InvalidateTokenDialog.tsx b/frontend/components/ngo/InvalidateTokenDialog.tsx index abc2510..2159f3b 100644 --- a/frontend/components/ngo/InvalidateTokenDialog.tsx +++ b/frontend/components/ngo/InvalidateTokenDialog.tsx @@ -1,6 +1,6 @@ import { useAuthStore, AuthState } from '../Login' import { fetcher } from '../../codegen/fetcher' -import { useNgoQuery, Created_Tokens } from '../../codegen/generates' +import { Created_Tokens } from '../../codegen/generates' import { useTranslation } from 'react-i18next'; import create from 'zustand' diff --git a/frontend/components/ngo/InviteSupervisor.tsx b/frontend/components/ngo/InviteSupervisor.tsx index 120cb5a..b557cd2 100644 --- a/frontend/components/ngo/InviteSupervisor.tsx +++ b/frontend/components/ngo/InviteSupervisor.tsx @@ -1,8 +1,10 @@ import { useEffect } from 'react' import { useAuthStore, AuthState, jwtFromLocalStorage } from '../Login' +import { SupervisorPasswordReset } from './SupervisorPasswordReset' import { fetcher } from '../../codegen/fetcher' -import { useNgoQuery } from '../../codegen/generates' -import { useTranslation, Trans } from 'react-i18next'; +import { useNgoQuery, Supervisors_Registered } from '../../codegen/generates' +import { useTranslation } from 'react-i18next'; +import {Supervisor} from '../user/Supervisor' async function mutate(auth: AuthState, mail: string) { const result = await fetcher(`mutation Invite($auth: Auth!, $mail: String) { @@ -20,8 +22,8 @@ export function InviteSupervisor() { }, [auth.jwt]) const { data, remove, refetch } = useNgoQuery({auth}, {enabled: Boolean(auth.jwt)}) - const registered_active = data?.supervisors_registered?.filter(s => s.name_full) - const registered_new = data?.supervisors_registered?.filter(s => !s.name_full) + const registered_active = data?.supervisors_registered?.filter((s: Supervisors_Registered) => s.name_full) + const registered_new = data?.supervisors_registered?.filter((s: Supervisors_Registered) => !s.name_full) return auth.jwt && (
@@ -43,13 +45,23 @@ export function InviteSupervisor() { { Boolean(registered_active?.length) && <>

{registered_active?.length} { t('Active registered supervisors') }:

-

{ registered_active?.map(s => s.name_full + ' (' + s.mail + ')').join(', ') }

+ { registered_active?.map((s: Supervisors_Registered) => +

+ { s.name_full + ' (' + s.mail + ')' }   + +

+ ) } } { Boolean(registered_new?.length) && <>
{registered_new?.length} { t('New registered supervisors') }:
-

{ registered_new?.map(s => s.mail).join(', ') }

+ { registered_new?.map((s: Supervisors_Registered) => +

+ { s.mail }   + +

+ ) } } diff --git a/frontend/components/ngo/SupervisorPasswordReset.tsx b/frontend/components/ngo/SupervisorPasswordReset.tsx new file mode 100644 index 0000000..f6e3738 --- /dev/null +++ b/frontend/components/ngo/SupervisorPasswordReset.tsx @@ -0,0 +1,45 @@ +import { useAuthStore, AuthState } from '../Login' +import { fetcher } from '../../codegen/fetcher' +import { useTranslation } from 'react-i18next'; +import create from 'zustand' + +type SupervisorMail = string; +type SupervisorPassword = string; + +interface PasswortMap { + [Key: SupervisorMail]: SupervisorPassword; +} + +export interface PasswortMapState { + passwortMap: PasswortMap, + addPasswort: (mail: SupervisorMail, password: SupervisorPassword) => void, +} + +export const usePasswortMapStore = create(set => ({ + passwortMap: {}, + addPasswort: (mail, password) => set( orig => ({passwortMap: {...orig.passwortMap, [mail]: password}})), +})) + +async function pwReset (auth: AuthState, supervisor: string, addPasswort: PasswortMapState["addPasswort"]) { + const result = await fetcher(`mutation SupervisorPasswordReset($auth: Auth!, $supervisor: SupervisorMail!) { + supervisor_password_reset(auth: $auth, supervisor: $supervisor){mail, password} }`, + {auth, supervisor})() + //console.log(result) + const password = result.supervisor_password_reset.password + addPasswort(supervisor, password) +} + +export function SupervisorPasswordReset({supervisor}: {supervisor: string}) { + const {t} = useTranslation() + const auth = useAuthStore() + const {passwortMap, addPasswort} = usePasswortMapStore() + + return ( + <> + { await pwReset(auth, supervisor, addPasswort) } } + value={ t('Reset Password') as string } /*name={supervisor}*/ /> +   + { passwortMap[supervisor] } + + ) +} diff --git a/frontend/pages/supervisor/edit.tsx b/frontend/pages/supervisor/edit.tsx index 71b01de..02a0b20 100644 --- a/frontend/pages/supervisor/edit.tsx +++ b/frontend/pages/supervisor/edit.tsx @@ -197,6 +197,12 @@ export default function SupervisorEdit() { { t('Name') } + + { t('job_title') } + + + + { t('Specialization') } @@ -204,7 +210,7 @@ export default function SupervisorEdit() { - +
{ t('Specialization_examples') }