In this chapter, we're going to implement a WAPI plugin, allowing the users to add their API keys, and we are going to use the AWS secrets from the previous chapter we've set up. Let's start by adding AWS secrets library to our project. So go ahead and run pnpm f backend add aws-sdk-client-secrets-manager. Once this has installed, double check that you have it inside of your package.json inside of packages.backend. As you can see, I'm using version 3.859.0.
You don't have to use the exact same version but if you see a change in the major version such as 4 or 5 there might be some differences in the API but I suggest that you follow along or install the same version as me and then if some problems occur you're going to have to look at the documentation. Let's start by going inside of packages, backend, convex, and let's go inside of lib. Inside of here, create secrets.ts. Let's go ahead and let's import everything we need from AWS SDK Client Secrets Manager. We're going to start by importing the create secret command.
After that, let's add get secret value command. Then, let's import the type get secret value command output. And then let's add a few more commands. PutSecretValueCommand, resourceExistsException, and SecretsManagerClient. Now let's export function createSecretsManagerClient.
Its return type will be SecretsManagerClient. Inside of here, let's return new SecretsManagerClient. Inside of region, go ahead and pass process.environment.awsRegion. Inside of credentials open an object here and add access key ID to be process dot environment dot AWS access key ID or an empty string and same thing for secret-access-key. Let's go ahead and set up our environment local in the backend.
Let's just double check from here. So I like to copy environment names because it's always safer this way. You can sometimes misspell things and not notice them. Just like that. Same thing with AWS region.
What's super important is that you have also added this to your convex account so just double check inside of settings environment variables that you have all of them and that they are named exactly the same. Great! Now we have the create secrets manager client here. Now let's go ahead and let's implement a function to get secret value. Expert function get secret value.
It will accept secret name as the prop which will be a type of string and it will return a promise which resolves to get secret value command output. Let's go ahead and define the client in here using create secrets manager client and let's simply return await client dot send new get secret value command and inside secret ID will be secret name Let's go ahead and make sure that this is an asynchronous function. There we go. And now we shouldn't have any errors. Now let's go ahead and export another asynchronous function called AbsurdSecret.
This will accept SecretName, which is a type of string and secret value. The value will be a record string and unknown. So yes, we're going to store objects here and we're going to stringify them so they are easily accessible inside of the AWS dashboard and so that we have a more advanced type that we can work with because storing them individually is kind of a problem because it's easier for us to simply ask the user for all of their API keys from a single service like WAPI and then stringify that and save that rather than creating individual secrets. That's why we're going to be working with objects. And the return here will be a promise and void.
Let's first define the client here to be create secret secrets manager client and let's go ahead and open a try and catch. Instead of try here let's await client.send and we are now going and we are now going to try new create secret command. Name will be secret name and secret string will be json stringify secret value. That's how we're going to store new secrets. But in case an error happens, it can happen for multiple reasons.
But if the error is instance of resource exists exception that means that the user already has this secret in that case what we are going to do is we are simply going to update it so client dot send new put secret value command secret ID secret name secret string again json stringify secret value as simple as that And in the else here we're simply going to throw the error because it's not something that we can handle. Great! We now have a function that allows us to either create the new secret or if we notice that we already have that value we are simply going to update it with new API keys. And now let's create a simple helper to help us parse the value because we store it as a JSON string. So let's export function, parse secret string, let's go ahead and set T to be type of record, string unknown, The prop here will be get secret value command output.
And it will return T or null. Now let's go ahead and check if we don't have secret.secretString let's return null. There's nothing to parse. Otherwise, let's try and return JSON.parse secret.secretString as T else let's return null. My apologies, not else, catch.
There we go. So now we have a simple method which can help us read the value of the getSecretValue command output. Right, so when we call this getSecretValue we're going to get it back inside of JSON stringify. So in order to work with it, we're going to use this little util to help us turn that secret string into an actual object. And we are going to be able to type that object thanks to this little extension.
So however we use this function we're going to be able to give it specific types. So for example parse secret string and then in here we're going to be able to do id or let's say public api key string like this And then the return of this function will be public API key string strictly typed. That's what this part does. It allows us to dynamically set the return type. Excellent, We now have our secrets library.
Now let's go ahead and let's create an internal action for working with secrets. So inside of system go ahead and go ahead and create secrets.ts. Let's import v from convex values. Let's import internal from generated API. Let's import internal action from generated server and let's import our newly created absurd secret from lib secrets.
Let's export const absurd internal action arguments organization id service will be a type of union and the only option available here for now will be puppy and the value will be any Now let's add the handler here which is going to be an asynchronous method like this. Now in order for this to actually make more sense, it would be better if we paused here and created the schema for our plugins. So let's quickly go inside of convex schema.ts And just above the conversations let's add plugins. Let's go ahead and call the find table. And inside let's add organization id to be a type of string, service of this plugin will be a union and inside you would add all the services you support.
In my case that's only going to be WAPI but I'm building this with keeping in mind that you might want to add multiple plugins right because take a look at how I've designed this I've made it so it's ambiguous imagine this is Google Calendar right then it will say Google Calendar integration Here a little image of Google would stand and you would click connect. So that's what I'm keeping in mind. You would probably want to extend this somehow. So that's how we are building this. But right now the only thing we support is wapi.
And one more thing we need here is the secret name. So the secret name will be used to access our secrets.ts lib here when we call get secret value. So later when we want to read from AWS secrets manager, hey what were the API keys for this organization who stored their WAPI keys with us and then we're going to return back, hey these are the keys that you have stored inside of AWS and then we're going to parse them so they are proper objects. So now that we have the plugins set here make sure you have organization id, service and secret name and now let's start by adding the index by organization ID and then let's add another index by organization and service. And actually I would like this to be, let me see, I just want to be consistent.
So, by contact session ID, yes, I want to put this by organization ID and service. I think that is more consistent with the way I've named my other indexes here. Great. Now, let's go ahead and do turbo dev. This way we can watch our convex dev run and see if we have any errors with our schema or with any functions we have written so far.
There we go. Added table indexes and convex functions ready. All good. Now we can go back and set up our internal upsert action here. Inside of here the first thing we're going to do is we're going to get our context and the arguments here and I don't think I have anywhere else used organization ID like in the param name except here in the secrets right especially if I try this V dot string Yeah, everywhere else it is organization ID.
Exactly. So organization ID. Let's be consistent. So first things first, let's generate the secret name. How do we name our secrets?
Well, the answer lies inside of our AWS actually. If you remember inside of our echo tenant secrets manager access we created an ARN And I'm not really sure how to like read it properly here, but we made it a wild card that has to be used in a form of tenant and then asterisk after that. If you remember, If you don't, you can quickly go back to the chapter to see what I'm talking about. But basically, that's how we need to build our secret name, tenant, and then we're going to pass arguments organization ID, and then we're going to add arguments.service. So this will end up being tenant 1234 as in organization ID or tenant ID, right?
We are using the word tenant within AWS. And then WAPI. And then it may be in the future, Google or cowl.com or Vercel or whatever you might do. In our case this is how it's going to be. And now let's call await absurd secret, secret name and value.
Let's go ahead and pass arguments.value here like this and let's return status success And now let's go ahead and also create the plugin record in our database. Because right now when the user calls this action, well, user will never call this action because it is internal but even when we call it sure AWS will receive this new secret under this specific tenant name but we are never actually going to store the secret name inside of our table so we will never know how to retrieve it back. And let's go ahead and do export const absurd internal internal mutation the arguments will be service which is a type of union and accepts for now only wapi Secret name will be a type of string and organization ID will be a type of string as well. Now let's add the handler and let's go ahead and get context and arguments. First, let's find if we already have a plugin.
So existing plugin, await context.database, query our newly created plugins with index by organization id and service. Let's go ahead and grab q here and let's go ahead and check if organization id equals arguments organization ID, and if service equals arguments.service. So we are specifically looking for a database record, for a unique database record with this organization ID and this service. There should never be two records like that. If we have an existing plugin, in that case, let's go ahead and do a WaitContextDatabasePatch existingPlugin underscore id service arguments dot service secret name this is super important to store secret name arguments dot secret name else let's go ahead and do await context database insert into plugins.
And we're creating one for the first time. So we need to pass the organization ID as well. And then we can just copy service and service name from above. Just like that. Perfect.
Now that we have this, let me just add this and let's build one more internal function here. Get by organization id and service internal query const the arguments here are going to be organization id and service. Let me just quickly copy this again and then we're going to have the handler let's grab the context and the arguments here and let's simply do return await context.database query plugins with index and we can just copy from here like this so by organization id and service organization id service arguments service. Perfect exactly what we need. Now that we have this let's go ahead and let's go back inside of our secrets.ts right here in the convex system folder so we've just upserted this secret where we generate the secret name and now since this is an internal action we cannot directly access the database So what we have to do is await context run mutation internal system plugins absurd and then we pass in the service arguments service secret name and organization id arguments organization id Just like that.
Now we have synchronized both the AWS and our internal database. Perfect. Great. Now that we have this, we are ready to start building the components. So let me just go ahead and mark these as completed.
Secret Slib, Plugin Schema, Secrets functions. Now we need WAPI view and plugin card component. Let's go ahead and let's build the WAPI view. So inside of apps, web, modules, let's go ahead and create a new module plugins inside let's create UI views and let's add wapi-view.tsx let's go ahead and mark this as use client and let's export const wapi-view whoops and let's return wapi-view now Let's go ahead inside of web app dashboard plugins wapi page.tsx and let's modify the return wapi-view Just like that Now when you go to localhost 3000 you should have it running. So web dev should be in localhost 3000.
And in here, when the app loads and you click inside of voice assistant here, you should see WAPI view. This is what we're going to be developing now. If for whatever reason it's not working, remember you have dashboard sidebar and in here you have the links, plugins, WAPI for voice assistant. That is this and now this. All right.
So in order to build this we're going to have to build that plug-in card but I just want to start by first adding a div class name flex minimum height of screen flex column BG muted and padding of 8. Then inside I'm going to create another div, mx-auto, full width, and a maximum screen MD. Then let's go ahead and create another div with a class name, space Y2, H1 element, WAPI integration. Actually, let's use the word plugin, not integration, because integrations will be something else in our app. Text to excel md text for excel below that connect WAPI to enable AI voice calls and phone support.
Let's go ahead and give this a class name text muted foreground. And in here let's go ahead and add a class name margin top 8 and let's go ahead and simply add to do plugin card let's see our app and how it looks like this is how it should look like and now let's develop the plugin card. So the plugin card will actually be a reusable component for again whatever services in the future you might want to have. Instead of the plugins module, instead of the UI folder, create components and let's create plugin-card.tsx let's import arrow left right icon type Lucid icon and plug icon all from Lucid React. Let's import image from next image.
Let's import button from workspace UI components button. Let's export interface feature here because we will pass a list of features inside of these plugin cards. So what are features? This. This will be controlled outside of this component.
We're going to pass them as props, as an array of features that some plugin has. So each feature will have an icon, which is a type of Lucid icon, label and description. And make sure to export that interface. Now let's create an interface plugin card props with option is disabled which is an optional boolean, service name which is a string, service image which is a string, features which is a type of array of features and on submit which is an arrow function. Perfect.
Now let's go ahead and let's export const plugin card. Let's go ahead and reuse the plugin card props here and let's go ahead and let's destructure all of them. So isDisabled, service name, service image, features and onSubmit. In here let's go ahead and return a div with a class name height fit full width rounded large border bg background and padding of 8. Already you can go inside of WAPI view and you can just render the plugin card.
Ignore the errors for now. But at this point, you should already see a white box here. Now let's continue developing the plugin card. So I'm going to add a div inside with a class name margin-bottom-of-6 flex-items-center justify-center-and-gap-6. Inside of this div I'm going to add another div with a class name flex-flex-column and items-center.
Now let's add an image, which is a self-closing tag with an out of service name, class name, rounded object contain, height of 40, width of 40 and source service image. Right now if you try you will see a broken image but just for fun You can pass in the service image here to be logo.svg and you can pass the service name already to be wapi. Great. Now let's go back inside of the plugin card. Outside of this div, Let's go ahead and create a new div and do arrow left right icon.
And let's go ahead and give this a class name flex, flex column, items center and gap one. Perfect. And now let's go ahead and add, we can actually copy this entire div, paste it here. And this will be your platform, right? So platform, It doesn't need this and the source will always be logo SVG.
So now we're just going to have two of the same images, right? So let me just go ahead and quickly add the VAPI logo into my assets so that you can replace it and so that this image makes more sense. So head to my echo assets repository, you can see the link on the screen here and inside of the public folder you will have wapi.jpg And then go ahead inside of your apps, that public folder and simply paste it inside or drag and drop it. And now let's go ahead and go inside of our WAPI view and replace this with WAPI.jpg. And there we go.
Now this makes more sense, right? So we are connecting wapi with our application perfect so let me just see let's go back inside of the plugin card here so we continue the development. After this and after this div actually here, let's create another div with the class name, margin bottom of 6 and text center inside a paragraph with a class name text large span inside connect your service name account Let's go ahead and check that out. There we go. And in fact, span is not needed here.
I had an idea, but it's okay like this. Outside of this div here, let's create our feature list. So another div with a class name margin-bottom-of-six inside another div with a class name space y4 and then in here let's do features dot map and for each feature let's go ahead and let's return a div with the class name flex items center gap 3 key feature dot label with the class name flex-items-center-gap-3-key-feature.label another div inside like this with a class name flex-size-8-items-center-justify-center-rounded-large with a class name flex-size-8-items-center justify-center-rounded-large-border-background-muted and inside feature.icon, a self-closing tag with a class name size 4 and text muted foreground right now nothing will be visible in fact we're going to have an error That's because our type here is incorrect. So let's go ahead and let's pass the WAPI features. So this is how I'm going to do that.
I'm going to add const WAPI features right here. And I will use the feature type from plugin card. So the first one will be icon globe icon from Lucid React, label web voice calls and description is going to be voice chat directly in your app. Let's go ahead and let's pass in the features here. WAPI features And now you should be able to see the icon.
Just like that. Now let's go back inside of the plugin card and let's finish the features list. So outside of this div, another div. And in here we're going to render feature.label with a class name font medium text small let me just fix the typo here. Copy that.
Change these to feature description. And this one will be text extra small. And text muted foreground. Just like this. Now I'm going to add the rest of WAPI features.
So back inside of WAPI view, I'm going to add all the icons. Globe icon, phone call icon, phone icon, and workflow icon. And then I'm just going to show you each feature one by one. So the next feature after web voice calls we're going to have phone numbers and a description matching and now I'm just adding some features to populate this plugin card. So phone call icon, outbound calls, automated customer outreach.
Basically, all the things you can do with WAPI, right? One more. Workflow icon, workflows, custom conversation flows. Save this. And now this looks much better.
Very attractive to add this to your app. Perfect. And now let's go ahead and go back inside of the plugin card. And now outside of this div here, create a new div with a class name text center, add a button which we already have, inside connect and plug icon. Let's go ahead and give this the class name, size full, disabled, is disabled, on click, on submit, and variant default.
Just like that, we have finished our plugin card reusable component. And you can now reuse this to create all the future plugins that you want to have in your app. Great. So now, let's go ahead and pass in some more things. So we have service image, we have service name.
Is disabled. Let's explicitly add it to false for now. And on submit, Let's also explicitly add it like that. So what would be the easiest thing to do now? Well, what I want to do right now actually is I want to be able to load a plugin.
In order to load a plugin, we are already halfway there because we have the schema for our plugins and we have the plugins.ts internal functions to create them And we can also fetch them. But we don't really have a function that will allow us in here to call useQuery, VAPIPlugins and then so we can check is this connected or not, right? Because this is obviously disconnected state. But if we manage to load WAPI plugin with this organization id this should change into the actual well fetched phone numbers and voice assistants right So let's go ahead and leave the WAPI view as it is now. And let's go back inside of our packages backend convex.
And inside of private, let's create plugins.ts. Let's export const get1, which is going to be a type of query and it accepts a service, which is a type of union literal WAPI. Let's go ahead and let's import query from generated server. Let's import convex values. Let's add an asynchronous handler, context arguments.
Now let me quickly go inside of messages here for a very simple reason so I can copy the identity check and the organization check. Let's quickly go back, plugins private, paste this here. Let's import convex error from the values here like this. And now once we've confirmed we have organization ID, let's simply return await context dot database query plugins. And then we simply do the index we've already done before by organization id and service comparing the organization id with the org id and service from arguments dot service that we pass in here.
Make sure it's unique and that you are returning that. Great. And while you are here actually we can go ahead and do something more. Let's copy this entire thing and just above here, paste it and rename this to remove and instead use mutation from generated server. The check is the same.
Identity is required. Organization ID is required, But instead of returning this, we're gonna do const existingPlugin. Like this. And then if there is no existing plugin, throw new convex error code not found message plugin not found. So we can't delete it.
And now let's do await, context database delete, existing plugin underscore id, and let's return existing plugin underscore ID. Actually, we don't have to return anything. So now we also have our remove mutation here. Perfect. Now we can go back inside of the WAPI view.
And in here, we can get the wapi plugin by using use query from convex react and in here we can pass in the api from workspace backend generated api api.private.plugins.get1 backend generated API. Api.private.plugins.get1 and in here let's go ahead and pass service wapi so if this is a type error for you it means you don't have your back-end running so just make sure you have back-end running and no errors here. So now we're going to be able to know do we already have existing WAPI plugin or not. So when will this be disabled? It's gonna be disabled if VAPI plugin is undefined because that is the loading state in convex when you use useQuery.
So now whenever you refresh this page for a brief second, you will see how it is disabled because it's still loading that query. Perfect. So now in here, we can do, if we have WAPI plugin, in that case, we're going to display one thing otherwise we're going to display plugin card so in here I'm just going to say paragraph connected like this But for now since we don't have that record in our database it's always going to be this integration form. What we have to do now is we have to develop the actual integration form. In order to do that we're going to need a couple of states.
Connect open and set connect open. Let's call useState and pass in false. Let's import useState from react and now let's also import everything we need from the dialog. So in here, dialog, content, description, footer, header, and title. Let's also import everything we need from form.
Form, form control, field, item, and form control, field, item and message. Now let's also import input and label components. Then let's add Zod and let's add React Hook form and let's add our Hook form resolvers. And just because I already promised you a couple of chapters, Let's go ahead and let's add the toast component. We already have the toast component.
We just have to go inside of apps, web, app, layout and in here render toaster. Let's go ahead and import toaster, toaster from workspace UI components, toaster, my apologies, sonar, like this. Now that you have the toaster, you will be able to toast everywhere. So yes, you have the sonar package already installed inside of packages UI. But now we have a small problem here.
While we can add this toaster instead of our app layout.tsx if you go back instead of wapi-view and for example in here you want to import toast from sonar right you don't have sonar here because this is a different package. If you search for sonar you can see we have it but inside of packages UI. So now let's copy this version and let's do pnpm f web add sonar at and then this specific version. And this will then allow us to have the same version inside of the web package, I mean, web app. And now no more errors here.
So we can finally use Sonar now. Great! Now let's go ahead and let's develop the form schema here. So after WAPI features let's add form schema which will be a Zod object which accepts public API key and private API key both of these are going to be strings and let's go ahead and give them minimum one with message public API key is required. And then we can copy this, add it here.
Private API key is required. Great. Now, let's go ahead and let's export. My apologies, no need to export anything. We can just do const wapi-plugin-form.
The types this component is going to have is going to be open which is a boolean and set open which will control the value, which is essentially the open value, right? Open, setOpen. Now in here, let's go ahead and use our absurd secret from use mutation from convex react API private secrets. Dot absurd. And is it possible that I forgot to do this?
I thought we had all the functions we need. Looks like there is one more that we need. My apologies. So let's quickly add this absurd. Inside of packages back and convex private, go ahead and create secrets.ds.
And do we have anything similar okay let's just go ahead and do it so export const absurd is a mutation the arguments that is going to accept our service and value The handler is going to be an asynchronous method. Let's import the convex values. Make sure to add literal VAPI here because that's the only one we support right now. As always, let's go ahead and just copy from the plugins here our identity and organization id check. Let's import the convex error from the values.
And now once we did this, let's go ahead and add to do check for subscription, because we shouldn't be able to do this if we don't have subscription. And now let's go ahead and let's upsert our secret, Except we have a small problem. How can we do that? How do we call internal functions within mutations? So, this is just a way for us to call this using useMutation from the WAPI view here.
But the actual logic for creating AWS secrets lies inside of convex system secrets. Remember? Absurd, which accepts organization ID, service, and value that we are trying to save. In here, we generate the secret name. We store this inside of AWS using the absurd secret from our secrets lib.
And then we also save this to the table to the database. So the way we have to do this is by using await context scheduler run after zero as in immediately internal make sure to import internal from generated api.system.secrets.absurd and simply pass in the service to be arguments.service passing the organization id this will be org ID and passing the value arguments that value. Let's just check if I did this correctly. So we have the value. Oh, I completely ruined this.
Internal. So I think this type should work, right? If I forgot to add something, I have an error. If I add a typo, I have an error. Perfect.
So it's strictly typed. Make sure that you have passed the service, which is WAPI organization ID, so we can build a secret key and the value, which will be from here, from our form schema, public API key and public and private API key, but we're going to stringify them, right? Because when we receive that here, absurd secret, we're going to stringify that object. So it will be stored as a string. Perfect.
So you might have a question here. Why not just use use action? Why did I abstract absurd behind the mutation? So we are kind of doing half the work here and then calling this. Well, for one reason, I feel like secret Absurd is more reusable this way because Absurd definitely has to be an action because it uses this Absurd Secret which is third party.
This entire thing is third party so it has to be an action but the reason I've labeled this as an internal action because I feel like this makes it way more reliable right if we ever want to create an update form we we can just easily create another mutation and then schedule the internal absurd like this. Another reason is because using use action while completely okay is labeled inside of convex documentation as an anti-pattern. So that's why I'm trying to write as good code as possible here for you. So I wrote an internal action here and then I'm wrapping it within mutation, which is exposed. And I know that kind of mixing this internal stuff, actions, mutations, and queries is a little bit complicated.
So in case you're confused, it's perfectly fine. These are new concepts, right? Convex is a very specific runtime. It's super powerful, but it takes time to learn and understand. So if it helps, you can write down some flows, the differences between the two.
But the thing that can help you the most is their documentation. Whenever you feel stuck, have no idea what I'm talking about go ahead and search mutation, convex documentation, internal action why do we need action, what's a third party, all of those things, right? Great, now we have the absurd mutation here which calls our previously created secrets here. Now that we have that, let's go back in here and let's go ahead and let's define the form. We've already defined the form constant, I think five times already.
So constant form is use form z.infer type of form schema from above, Zodd resolver, form schema, default values, public API key and private API key. Perfect. Now let's develop const on submit method. This is going to be values z.infer type of form schema again. It's asynchronous.
Let's go ahead and open try and catch method here. And for the first time, let's use toast dot error. Something went wrong. And let's also actually do console error simply so our users can report like hey I'm seeing errors in the console and I mean yeah I'm not sure how smart of an idea this is, but at least in the development it might be a good idea for you to see the entire error in the console. And now let's do await absurd secret here, passing the service that we want to save which is WAPI and then the value it can be anything in our case public key data public my apologies values public API key, private key, values, private API key.
Yeah, actually let's store them like this. Public API key, private API key, right? So no point in not calling them the same. All right. And now we have this onSubmit method.
And now let's go ahead and let's return Dialog. Let's go ahead and pass onOpenChange to be setOpen and open to be open. Let's add a dialog content, dialog header, dialog title, enable WAPI. Outside of header let's add dialog description and in here let's just add something descriptive like your API keys are safely encrypted and stored using AWS secrets manager And then in here let's add our form. Let's spread the form.
Let's use normal form element. Give it class name flex, flex column, gap y4. And let's pass on submit, form handle submit, on submit. Form field is a self-closing tag which has control form dot control so again this is just chat cn and react cook composition here The first field will be public API key. Let's go ahead and destruct field here.
Form item, label, public API key, form control, input, and let's spread the entire field. And let's go ahead and set placeholder here, your public API key. And the correct type here would be password. And let's add form message below it. Now we can copy this entire form field, paste it.
This one is going to be private API key, your private API key, private API key. There we go. And now still inside of, still inside of form here, add dialog footer, render button inside. We have to import it first. So make sure you have added the button from Workspace's UI Components button.
If form form state is submitting, write connecting, otherwise connect. Disabled If form form state is submitting, type submit. Now we have to render it and we have to enable it on click. I mean on click of our connect button here. So let's scroll down inside of WAPI view, wrap our entire app inside of a fragment so we have semantically correct positioning of our components and let's add wapi-plugin-form.
Let's pass in open, connect open, let's pass in set open, set connect open, like this and now let's develop const handleSubmit if we have wapi-plugin we're going to set removeOpen which currently doesn't exist so let's quickly just create it. Remove Open Set Remove Open This will be for removing the connection. So Set Remove Open Set to True Otherwise Set Connect Open True Like this Make sure to pass connect open and set connect open to the WAPI plugin form here. And in the plugin card, in here, let's simply pass handle submit. As simple as that.
When you click connect now you will have a new model which allows you to add your API keys. So we now have to test if all of this is actually working. Let's try it out. What should happen first? Once I add some keys and click connect, what should happen is that inside of my data here, inside of plugins, I should have my first record.
The second thing is I should have a new secret inside of AWS. So just for easier visibility I'm going to change the type to text here for my API keys just for us so it's easier to develop. I will say for the public key, public one, two, three, private three, two, one, and let's click connect. And now you can see immediately this was connected, but one thing that I don't like is that this didn't close so let's quickly fix that inside of WAPI plugin form on submit here after successful upsert set open to false and we can also do toast success WAPI plugin. Should we do it?
Yeah, let's do WAPI secret created. That's technically what happened. Alright, so now as you can see, I already know the record here is created because it says connected. You can see how it still tries to load it initially and then when it loads, it says, hey, I have this record here, plugins. And you can see how in our database, we don't store any API keys.
So if someone breaches our database, we are not storing any sensitive information. And if they grab the secret name, they also have to obtain our AWS access key, AWS secret access key and our region. And keep in mind, we can just change this. We can just remove this, right? So we are protected.
And this is fine, but the real question is, do we have it inside of here? So let's go ahead and go inside of our dashboard here and type in secrets manager. Go ahead and click here and let's see if this works or not. So I will probably have a bunch of them here. But let me go ahead and try clicking on the latest one.
You don't have all of this. I have them because obviously I was developing this app. But let me try clicking on the latest one assuming that this is the newest one and in here you can click retrieve secret value keep in mind that you probably have to be logged in with your root user account for that let's click retrieve secret value and here it is private API key private 3 to 1 public API key public 1 2 3 Exactly what we stored was encrypted and stored securely inside of AWS Secrets Manager. And using this secret name and our combination of AWS keys here, we will be able to up cert and retrieve those API keys, and we are successfully one step closer to white labeling this entire thing. What an amazing job you've done for this chapter and what an amazing job you've done in the previous chapter.
Everything works. In the next chapter our job will be to display the phone numbers and the AI assistants that we developed in chapter 6 if you remember. So we are now going to load Tom and the phone number we created and this will officially allow every single customer of ours to create their own assistants, their own phone numbers, their own knowledge base. Amazing! Now let's go ahead and merge this.
So 24 WAPI plugin. And yeah, we can test one more thing though. Instead of WAPI view, yeah, try and do this. Always allow plugin card. And for now, always open set connect open in the handle submit.
So this is important, right? And Now even though we have API keys, we can now change them. So I will do change 123, change private 321. So change public 123, change private 321. Let's click connect.
Again we get the WAPI secret created but this is actually a successful upsert action. So now in here as you can see nothing has changed because it's still the same organization and the same service. So nothing here has changed. These plugins didn't even update I think. It's still the same secret name.
The only thing that should update is inside of AWS secrets manager again you should probably only have one I have a bunch of them select this one let's click retrieve secret value change private three to one change public one to three What an amazing job you've done here. Excellent! So I will now just bring all of that back. Handle submit should look like this and this should be conditional. So now inside of here you should see connected.
Amazing! 24 WAPI plugin. Let's review and merge these changes. I'm adding the commit 24 WAPI plugin. Let's click commit.
I'm going to go ahead and create a new branch 24 WAPI plugin and I will publish the branch. And I'm very very curious what CodeRabbit will think of this VR. I think we did a really good job but I am interested to see what a professional says. So let's go ahead and create this pull request and review it. And here we have the summary by CodeRabbit.
We introduced a plugin integration UI for connecting and managing the WAPI service, including a secure form for submitting API keys. We added a plugin card component to visually display external service connection options and features. We implemented global TOS notifications for improved user feedback throughout the app. The WAPI plugin page now provides an interactive view for connection status and management. As per the infrastructure, we added backend support for securely storing and retrieving plugin secrets and managing plugin records.
And in here we have the sequence diagram explaining exactly what happens. So let's go through it here. When the user clicks connect and submits API keys through the WAPI view on the front end, we call the absurd secret mutation with our API keys using the back-end API. Then we call the absurd secret from our secrets lib saving them inside of AWS secrets manager. After confirming via success or failure we do the same with our database record and then we simply show toast notification and update UI.
That's exactly what we did. And CodeRabbit left a lot of comments. 15 comments. Now most of these comments are simply improving errors such as this one. One thing we forgot was switch back to type password here you can see how security concern private API keys should be masked definitely.
So we are going to fix that here. In here, it doesn't exactly know our future plans. So it doesn't know that we're going to have the remove option, which will also have its own submit button. And we will use this currently unused state variables, right? So I'm going to skip through that here and we will develop the proper page for that and now in here it's recommending checking if we have the API keys.
I'm fine with the way this is right now because this will fail either way if we don't have those. For the secrets here it recommends introducing all the other type of error handling because we are only handling the resource exists exception but honestly I'm okay with just throwing for all others I don't need to explicitly handle them. But yes this is obviously a good comment right I'm just saying for the simplicity sake, I'm just throwing all other errors. You should definitely look into handling all of them individually. And now it finally got fed up with me adding this identity and organization check every single place here.
So it recommended that I should probably reuse it, validate user and organization. We could definitely extract that somewhere and then reuse it. And then it left a couple of comments like that. Same thing as this, where I use as a string, right? So it's finally got fed up of doing that.
So it's recommending me to refactor all of those things. But in here, it actually recommended me a very interesting thing. So this is an absurd which means either create or patch but we should never allow the organization id to be changed but I currently don't check for that. So I could check for that and throw the error if I detect that the submitted organization ID is different from what was currently saved in the plugin because technically another organization could hijack API keys from someone else's organization. So this is very, very good oversight here.
Perfect. In here again, it suggests better error handling. Same thing here, but overall, I don't think there was any serious security issues. There is this thing that they definitely want to do not enable the organization ID to accidentally change during patch. I still don't think this can happen maliciously, but still we should prevent this from happening.
Even though in patch, we don't even pass this. Okay, okay, Yeah. I will look into it. Yeah. I don't think it can be malicious though.
I think it can just be in some super duper edge case weird behavior, but I don't think it can be malicious but very good comment and let's go ahead and merge this now once we have confirmed the merge let's go back here change this to main branch and let's click on synchronize changes push and pull and once this has synchronized as always I like to double check with my graph here and I can see oh did I 22 and I just called this 20 oh yes no I'm correct it's 23 was just setting up AWS secrets manager I thought that I skipped a number but I didn't actually because 23 was our AWS secrets manager setup so we officially pushed to github amazing amazing job We have merged this and see you in the next chapter.