# Profil API (Sherpa API und Grüne API) - Definition for Sherpa and Gruene API - intendet for the `Solution 3` mentioned in https://github.com/verdigado/gruene-app-planung/issues/18 - `TODO` marks indicate topics not discussed yet or in need for clarification # Requirements - TODO Gruene API soll Ressourcen über uuidv4 - TODO Optimistic Locking für Profile - TODO Bestätigung Projektmanagement - (MVP) Eine neue Email Adresse in der App kann nur ausgewählt werden über Umweg (zunächst) über Mitgliedsdatenformular (Katja gab alles). Außer User ändert die Email über Keycloak ## Profil Attribute/Datenmodell - Profil ID: - Profile brauchen eine eigene ID - TODO brauchen users dann auch eine eigene, nich sherpa id?: Nein ist ok - Keycloak ID (eventuell) - TODO Nochmal klären ob/warum: Nein verworfen - Sherpa ID - Profil hat 1:1 Beziehung zu User - User darf seine eigene Sherpa ID sehen, aber Sherpa ID soll nicht für andere Mitglieder sichtbar sein - Username (Grünes Netz Username, nicht änderbar) - Email (Grünes Netz Login Email, existiert aktuell in Keycloak, LDAP) **soll änderbar sein** - Vorname (nicht änderbar) - Nachname (nicht änderbar) - Messengers - Threema - Telefonnummer - TODO Warum von vornerein mehere Telefonnummern (und Emails) ausschließen?: Vom Model her Arrays, aber zuerst nur EIN Eintrag speicherbar - Social Media - Unterstützt Platformen: Facebook, Instagram, Twitter, Mastodon - Typen sollen nicht dynamisch erweiterbar sein - URL sollte validiert werden, ob sie zum Typen passt - Sichtbarkeit öffentlicher Attribute kann konfiguriert werden - Email - Social Media - Chatbegrünung: Bei dem Social Media Eintrag für Chatbegünung soll man explizit festlegen können, ob er für andere Nutzer sichbar ist. Im Gegensatz zu anderen Social Media Accounts wird dieser nicht manuell eingetragen bzw. existiert immer. - TODO Profilbild - Blob oder Url? - Source Set? (weil Bild angeblich bis zu 1280px Breite) - Cache für Assets - TODO Interessen: Zusammfassung zu Tags - TODO Fähigkeiten: Zusammfassung zu Tags - TODO Ehrungen (Awards): Vertagt - TODO Parteifunktion/Mandate?: Vertragt, eventuell nicht MVP - TODO Mitgliedschaften: Rückgabe als ID Liste - TODO Flag zum aktivieren/deaktivieren des öffentlichen Profils?: Flag wird gebraucht ## Operationen/Abfragen - User kann sein eigenes (privates) Profil abrufen - User kann sein eigenes (privates) Profil updaten - Bestehende Sherpa-Mitgliedsdaten sollen als Vorauswahl angeboten werden - User kann anderes (öffentliches) Profil aufrufen (über Profil ID) - Attribute, die der Eigentümer des Profils als nicht sichtbar markiert hat, werden nicht zurückgegeben. - User kann andere (öffentliche) Profile suchen - Private Informationen werden wieder ausgelassen - TODO eventuell nicht voller Datensatz wie bei Profile über ID abrufen - Liste aller möglichen Fähigkeiten abrufen - Liste aller möglichen Interessen abrufen - Cache Invalidierung - Gruene API muss Endpunkte zur Cache Invalidierung bereitstellen. - Sherpa kann diese aufrufen wenn sich User Profil Ended - Liste aller Gliederungen abfragen - Pagination/Filterung übernimmt ggfs Gruene API, nicht Sherpa API - Einzelne Gliederung abfragen - TODO Ehrungen, Parteifunktion/Mandate, Mitgliedschaften - TODO Liste aller Gliederungen abrufen - TODO Profil erstellen wenn es noch nicht existiert? ### Email Updaten - Man kann keine neuen Email Adresse hinzufügen (App) - Änderung geht nur über Mitgliedsdaten formular (oder Keycloak) # Sherpa API ## Data Models Definition of the data models referenced in the Sherpa API endpoints below. ### Profile ```typescript interface Profile { /** uuidv4 */ id: string; /** sherpa user id */ userId: string; // TODO username/email is "owned" by Keycloak but should be saved in sherpa as well /** Gnetz username (Keylcoak) */ username: string; /** Gnetz email (Keycloak) */ // TODO: how to return email when user profile does not exist yet // the email can be returned from keycloak/ldap but there is not id active: boolean email: Email[]; socialMedia: SocialMedia[]; messenger: Messenger[]; phoneNumber: PhoneNumber[]; // skills: Skill[]; // interests: Interest[]; tags: Tag[], picture: Picture privacy: PrivacySettings; /** * list if division keys * only list of direct memberships, not implicit ones */ divisions: string[]; } interface Picture { url: string } enum Visibility { PRIVATE = "private", PUBLIC = "public", // FRIENDS_ONLY = "friends_only", } interface PrivacySettings { email?: Visibility; // username visibility setzt sich zusammen aus Sichtbarkeitseinstellungen für alle grünen tools chatbegruenung?: Visibility; } /** * Set of values for some attributes that can be used for preselection or suggestions on the Update User Profile View/Form. */ interface ProfileFormValues { /** * Distinct Union of emails: Kommunikationseinrichtung und Profile Email */ emails: string[]; socialMedia: SocialMedia[]; messengers: Messenger[]; phoneNumbers: PhoneNumber[]; } interface UpdateProfileDto { email: string; active: boolean phoneNumber: PhoneNumber; messengers: Messenger[]; socialMedia: SocialMedia[]; skills: string[]; interests: string[]; privacy: PrivacySettings; } enum SocialMediaType { FACEBOOK = "facebook", INSTAGRAM = "instagram", MASTODON = "mastodon", TWITTER = "twitter", CHATBEGRUENUNG = "chatbegruenung", } interface SocialMedia { id: string; type: SocialMediaType; url: string; } enum MessengerType { THREEMA = "threema", } interface Messenger { id: string; type: MessengerType; /** users id for the given messenger platform*/ externalId: string; } interface PhoneNumber { id: string; country: string; number: string; } interface Skill { id: string; tag: string; externalId: string; } interface Interest { id: string; tag: string; externalId: string; } // TODO: Alternative to Interest/Skill: use a common Tag object with a type attribute interface Tag { id: string; externalId: string; type: "interest" | "skill"; label: string; } ``` ### Division ```typescript interface Division { divisionKey: string; hierachy: HierachyType; level: DivisionLevel; /** * Bezeichnung der Gliederungsebene */ name1: string; // Bundesverband/Landesverband/Kreisverband/Ortsverband | Abteilung | Regionalverband /** * Name der jeweiligen Abteilung * Aalen */ name2: string; shortName: string; } // const divisionLevelOrder = ["BV", "LV"...] enum DivisonLevel { BV = "BV", // 1 LV = "LV", // 2 BEZV = "BezV", // 3 Bezirksverband (derzeit als Kreisverband) KV = "KV", // 4 OV = "OV", // 5 } enum HierachyType { GR = "GR", GJ = "GJ", KPV = "KPV", } ``` ## Endpoints - Basepath for endpoints is `https://sherpa.gruene.de/sherpa/ws/m2m` - TODO: Error Responses ### Get Profiles We need an endpoint to list all existing user profiles? ``` GET /gnetz/v2/profiles-ids ``` **Response** - `200`: `string[]` ``` POST /gnetz/v2/profiles ``` **Parameters** - `userIds` (string[], in body) - `profileIds` (string[], in body) **Response** - `200`: `Profil[]` ```jsonc { "id": "a15e62f1-e1e0-4784-8d1d-e837bf274316", "userId": "12345678", "username": "johndoe01", "email": "john.doe@example.com", "firstName": "John", "lastName": "Doe", "phoneNumber": { "id": "12345678", "prefix": "+49", "number": "1771234512345" }, "messengers": [ { "id": "12345678", "type": "threema", "externalId": "822d7a5a" } ], "socialMedia": [ { "id": "12345678", "type": "facebook", "url": "https://www.facebook.com/...." } ], "skills": [ { "id": "1", "tag": "Grafikdesign", "externalId": "101" } ], "interests": [ { "id": "2", "tag": "Zeitpolitik", "externalId": "102" } ] "privacy": { username: "private", email: "public", socialMedia: { chatbegruenung: "public" } } } ``` **Response** - `200`: `Profile[]` ### Get Profile Form Values Returns valid options to be sent in the `Update Profile` endpoint below. ``` GET /gnetz/v2/profiles/{profileId}/form-values ``` **Parameters** - `profileId` (string, in path) **Response** - `200`: `UserFormValues` ```jsonc= { "emails": [ { "id": "12345678", "address": "john1@example.com" }, { "id": "12345679", "address": "doe2@example.com" } ], "socialMedia": [ { "id": "12345678", "type": "facebook", "url": "https://facebook.com/..." } ], "messengers": [ { "id": "12345678", "type": "threema", "externalId": "148b3d8437" } ], "phoneNumbers": [ { "id": "12345678", "prefix": "+49", "number": "1234 12345 12345" }, { "id": "12345679", "prefix": "+49", "number": "543321 54321 543321" } ] } ``` ### Create Profile ``` POST /gnetz/v2/profiles ``` TODO: Payload ### Update Profile - The update payload must be a complete set of attributes (like a `PUT` update in REST APIs vs `PATCH`). This is neccessary to design a future proofe endpoint that supports optional attributes. The sherpa application cannot differentate between missing properties and `null` values. If we decided to do a merge patch and accept missing attributes we wouldn't be able do delete optional attributes. - For `email`, `phoneNumber`, `messenger`,`socialmedia`: The update value needs to be sent as an object. If an id is included, it means the update references an existing sherpa entity. If the id is missing it means sherpa should create a new entity. - TODO: Clarify that updating an email entity in sherpa is not possible. A user can update his profile by referencing an existing sherpa entity by id or create a new one when no id is given: updating of existing sherpa email not possible! - TODO: If a user does not select an existing email but enters the same address by hand the app should automatically use the email entry returned - TODO: The backend MUST validate that the address of a new email entry does not already exist. - TODO: How is it possible to delete entries this way? Form value entries will accumulate and user will get confused why old data is still present. ``` PUT /gnetz/v2/profiles/{profileId} ``` - TODO: Clarify if updating skills/interests only by id is OK **Parameters** - `profileId` (string, in path): user profile id in uuidv4 format - `UpdateProfileDto` (JSON, in body): complete set of changes to apply to the user profile ```jsonc= { "email": "john.doe@example.com", "phoneNumbers": [ { "country": "+49", "number": "" } ], "messengers": [ { "type": "threema", "externalId": "1232" } ], "socialMedia": [ { "id": "12345", "type": "facebook", "url": "https://facebook/..." }, { "type": "instagram", "url": "https://instagram/..." } ], "interests": ["101"], "skills": ["102"], "privacy": {} } ``` **Response** - `200`: `Profile` ### Find Interests (Already implemented) ``` GET /gnetz/v2/interests ``` **Parameters** None **Response** ```jsonc [ { "externalId": "101", "id": "1", "tag": "Zeitpolitik" } ] ``` ### Find Skills (Already implemented) ``` GET /gnetz/v2/skills ``` **Parameters** None **Response** ```jsonc [ { "externalId": "102", "id": "2", "tag": "Gafikdesign" } ] ``` ### Find Divisions ``` GET /gnetz/v2/divisions ``` **Parameters** **Response** - `200`: `Division[]` ```jsonc [ { "divisionKey": "10100101", "level": "OV", "name1": "Ortsverband", "name2": "Aalen", "shortName": "Aalen OV", "hierachy": "GR" } ] ``` ### Get Division TODO ``` GET /gnetz/v2/divisions/{divisionKey} ``` **Parameters** - `divisionKey`: (string, in path) **Response** - `200`: `Division` # Gruene API ## Data Models Definition of the data models referenced in the Gruene API endpoints below. If a referenced type is not listed here it is the same as in Sherpa API above. ### Private Profile ```typescript interface PrivateProfile { /** unique profile id in uuidv4 */ id: string; /** sherppa user id */ userId: string; // sherpa user id username: string; email: string; // Gnetz Email Address phoneNumber: PhoneNumber; messenger: Messenger[]; socialMedia: SocialMedia[]; interests: Interest[]; skills: Skill[]; privacy: PrivacySettings; formValues: { emails: (Email & { isCurrent: boolean })[]; phoneNumbers: PhoneNumber[]; messenger: Messenger[]; socialMedia: SocialMedia[]; }; } ``` ### Public Profile ```typescript interface PublicProfile { id: string; username?: string; email?: string; // Gnetz Email Address phoneNumber?: PhoneNumber; messenger: Messenger[]; socialMedia: SocialMedia[]; interests: Interest[]; skills: Skill[]; } ``` ### Division ```typescript interface Division { } ``` ## Endpoints ### Find Public Profiles An authenticated user can use this endpoint to search for other public profiles. ``` GET /profiles ``` **Parameters** - `search` (string, in query): search term to look up user profiles in `username`, `firstName`, `lastName` - `page` (string, in query, default `1`): page index - `limit` (string, in query, default `20`): amount of items to retrieve per page **Response** `200`: `PublicProfile[]` ```jsonc [] ``` ### Get Public Profile An authenticated user can use this endpoint to retrieve another users public profile by id. ``` GET /profiles/{profileId} ``` **Parameters** - `profileId` (string, in path) **Response** - `200`: `PublicProfile` ### Get Own Private Profile - Return the profile of the authenicated users. All properties will be returned. - Missing entries for social media are filled with `null` values (frontend requirement) - The current value is marked in `formValues` with an `isCurrent` boolean and should be the first entry in the list (frontend requirement) ``` GET /profiles/self ``` **Parameters** **Response** - `200`: `PrivateProfile` ```jsonc { "id": "44ebc244-3fbc-429e-9bb4-c03e4c1038a6", "userId": "12345678", "username": "johndoe1", "email": "john.doe@example.com", "picture": { "full": "static.gruene.de/...", "thumbnail": "static.gruene.de/...-thumbnail", }, "phoneNumber": { "id": "12345678", "country": "+49", "number": "177 12345 12345" }, "messengers": [ { "id": "10000001", "type": "threema", "externalId": "822d7a5a" } ], "socialMedia": [ { "type": "facebook", "url": null }, { "type": "instagram", "url": null }, { "id": "10000004", "type": "twitter", "url": "https://twitter.com" }, { "type": "mastodon", "url": null } ], "privacy": { "email": "private", "socialMedia": { "chatbegruenung": "private" } }, "formValues": { "emails": [ { "id": "1234", "address": "john.doe@example.com", "isCurrent": true }, { "id": "12345", "address": "john.doe2@example.com" } ], "phoneNumbers": [ // TODO: isCurrent needed here? ], "messengers": [ // TODO: isCurrent needed here? ], "socialMedia": [ { // TODO: isCurrent needed here? "type": "facebook", "url": "https://www.facebook.com/...." } ] } } ``` ### Update Own Private Profile ``` PUT /profiles/self ``` **Parameters** - `UpdateProfileDto` (json, in body) **Response** - `200`: `PrivateProfile` ### Update Own Profile Picture ``` POST /profiles/self/picture ``` ContentType: Diposition ### Find Skills ``` GET /profiles/skills ``` ### Find Interests ``` GET /profiles/interests ``` ### Find Divisions TODO **Parameters** ``` GET /divisions ``` ### Get Division TODO ``` GET /divisons/{divisionKey} ``` **Parameters** - `divisionKey` (string, in path) **Response** - `200`: `Division` ### Cache Invalidation Endpoint TODO ``` DELETE /application/cache/contents ``` **Parameters** **Response**