In this chapter, we're going to add credentials. Credentials will be a great addition to our previous chapter in which we've implemented three different AI nodes. The only problem is right now those AI nodes are using our API keys. But what we want is to allow the users of our platform to bring their API keys. So in order to do that, we need to implement something called a credential.
We're going to basically create a credential schema, TRPC router, client hooks, and then create the normal views that we have already implemented for workflows and workflow list. But we're going to do the same for credentials. So users will be able to paginate through their credentials, search for them, or maybe sort them by type. So let's start by adding the credential schema and slowly going all the way to this, which is adding the credential dropdown to each AI node, which will finally allow our users to select which credential or in other words which API key they want to use for that AI node and then you will finally be able to remove your API keys from the environment file. So let's start with the schema.
I'm going to go inside of Prisma, schema.prisma and let's go ahead and just above workflow let's create model credential. Let's go ahead and copy the ID because it's going to be the same name of each credential will be a required string value will be a string as well. Let's copy the timestamps. And let's create a relation with the user. So user ID is going to be a string and then user will be a foreign key relation so user a type of user relation fields user id references id on delete cascade and let me just zoom out so you can see how this looks in one line and another relation it's going to have will be with the node that it will be assigned to.
So now to fix these errors we also have to add them to their respective schemas. Let's start with the user one. So let's find user. It's right here. Great.
And now let's go ahead and just do credentials credential like this and then if you go ahead inside of the credential you can see that user is completely resolved Now we have to do the same for the node. So let's go ahead inside of model node and what we're going to do is the following. Let's add credential ID to be an optional string because not every node will need to have a credential, right? Only those which use API keys like AI nodes. So credential will be a type of credential, again optional, relation, fields, credential ID, references ID.
And this time we're not going to add cascade because if we remove a credential it shouldn't delete the node as well. The node is just going to fail but that's fine we don't want to alter someone's workflow just because they deleted a credential that they were using somewhere. So let's go ahead or perhaps maybe this won't even allow to delete a credential which is being used. So maybe we can explore on delete either no action or maybe set null. That could be one of the options, but for now, just leave it like this.
Great. So once we added credential ID and credential, both optional to model node, There should be no more errors here, either with the user relation or with the node relation. What we have to do now is we have to implement something called credential type. Now This isn't really required, but it will improve user experience. So let's create an enum credential type and let's give it OpenAI, Anthropic and Gemini.
And then let's go ahead and make another property inside of the credential model type, credential type. Basically, make it required. So user will have to choose, all right, for which one of these are you creating an api key for and then later if you have more models you can extend it This just makes it easier for the user to categorize their credentials, but you can of course choose if you want to do this or not. Once we have that added, let's go ahead and push those changes. Npx prisma migrate dev We can name this migration credential schema and once you press enter it should synchronize the database with your schema.
As always make sure you restart your next server and your ingest server. And then inside of your localhost 3000 if you have it running just make sure to refresh it. So I'm just going to do that. Instead of localhost 3000 just make sure it refreshes. Great.
So we've handled the schema. Now let's go ahead and let's add the router. So I'm going to go ahead inside of source features and let's go ahead and create credentials. Inside of credentials I'm going to create a server folder and let's go ahead and let's copy the workflows routers.ds and let's paste it here. And now we're gonna go ahead and rename that workflows routers to credentials.
So let me just go ahead and select this credentials router. So make sure you're doing that inside of your new credentials folder right here. You can immediately remove the execute one because we're not going to need it, which means you can also remove these unused imports. Great. We will have the create one and you can choose for yourself.
Do you want this to be a premium procedure or not? I'm gonna say yes, this should be a premium procedure. So only those who are subscribed can add credentials. Let's go ahead and add an input here because this will not be automatically named like workflow. This will be something user has to fill in and what user has to fill in is the following.
They have to give this a name. So let's go ahead and make this required. Name is required. Type, which will be an enum. Credentials type.
You can import this from generated Prisma, which we've just added. And finally, let's do the value. Basically, the API key. And let's change this to be value is required then besides context here we're also going to have the input. From here you can destructure name value and type from the input.
And let's go ahead and create a new credential here. So we can keep this return as is actually instead of doing prisma.workflow let's do prisma.credential.create let's go ahead and use name user is correct Let's remove the nodes object and let's go ahead and pass in the type and the value. And I'm just going to add a little to do here. Consider encrypting in production. So what's the deal with encryption and API keys?
Well, obviously, if you are storing other people's API keys, you should consider encrypting them. In fact, in my previous tutorial, which was B2B intercom clone called Echo, I used Amazon Secrets Manager to do this. But interestingly enough, when I told people that I did this, they were quite surprised because most of them consider API keys to be something you can easily rotate and delete if it gets leaked. That is technically true, but always think of if you have thousands of users and thousands of users trust you with their API keys and if your database gets compromised, even though it's not exactly the end of the world for them because they can rotate API keys quite easily, some of them probably didn't create an API key just for your page. They have probably been using the same API key everywhere.
And now because of your database leak, they have to get rid of all those other places where they have been using that API key. So yes, it is obviously not the best practice to store an API key as a string in your database. I would highly suggest looking into Amazon Secrets Manager to handle this. I have the exact tutorial doing this in my previous project, echo. I will leave a link somewhere here on the screen so you can take a look at it.
But then again, I have heard of people just storing API keys in the database plainly like this, because yes, they can be very easily rotated from the dashboard. So for now let's go ahead and do this but please add a comment like this at least so you are aware that this is something you should consider doing. Perfect. So we can now successfully create the credential. Now let's go ahead and let's do the remove one because it should be quite simple as well so we need the ID and instead of workflow we're doing credential.
There we go. And let me just see. Delete. All right. So this will basically throw if not available.
Yeah, I think this is perfectly fine as is. Now let's go ahead and let's implement the update one. So again, I like to leave these to be protected procedures rather than premium ones simply because it's better user experience. Then again we don't need nodes, we don't need edges, but what we do need is name, type and value so you can just copy those from above and add them here. Then you can destructure the name, the type and the value.
Instead of workflow here we can do credential find unique or throw and let's go ahead and actually remove this entire thing. We don't need it to be this complicated at all. So let's just do if there is no credential. Actually, Yeah, that should not be possible because we're using find unique or throw. So what we can do is we can just update it const updated credential, or let's just do return prisma dot credential dot update where ID is the ID user ID is contact out user ID.
And let's pass in the data with the name, the type and the value. And let's go ahead and add to do consider encrypting the same comment we added above. So consider encrypting in production. And in fact, we don't even need this then, because this will throw if it doesn't exist or if the user ID is invalid. There we go.
So a very simple update procedure. We don't need the update name procedure for this one. So we have handled remove, we have handled update, let's handle get one. Again, protect procedure, z.string for the id, context and input right here and this time we don't need to do any of this transformation. The code should be much much simpler now.
We can just do return directly prisma.credential find unique or throw remove include and that's it. I'm pretty sure that's the only thing we have to do. Get one, finished. Now we have to implement get many, So again protected procedure. We will still have the page, page size, number, minimum, maximum, default, search, extract all of them here and we still need a promise all for items and the total count.
Let's go ahead and do prisma.credential.findMany. I think everything here can stay exactly the same. Make sure you change this to credential too. And let's see. So where user ID, name, containsMode, perfect.
And we could do the following here we could go inside of Prisma credential find many We could manually select ID, name, type, created at, updated at, but purposely don't add value for security since we are now storing it as plain text. So at least at this level let's make sure we don't show it to everyone, right? And we could in fact do the same thing instead of our get one here. So select. Let's go ahead and just do the exact same thing.
ID name type created at and updated app. But then again, since we are using the user ID, they are the ones who are allowed to see that. So maybe we are just creating problems here. Let's remove this like sorry for changing my mind so often. But this is the way I develop apps, right?
I change my mind often and I try to come to some solution that I like. So for now, yes, since this is not a public API, it is very strictly for this user. They kind of already know what their API keys are. So let's leave them here. All right so the logic for total pages has next page has previous page is exactly the same so no need to change this at all.
One more thing will be added here which will be called a get by type. Get by getByType will be a protected procedure. It will have an input, which will be an object and the object will very simply accept the type enum. And then let's go ahead and do query, asynchronous, extract input and context. Let me go ahead and fix this.
There we go. And then inside of here, let's go ahead and destructure the type from the input. Let's go ahead and fetch all credentials from await Prisma credential find many where we have a matching type and user ID is context out user ID. So for this one we won't add any pagination simply because this will be used inside of a drop down. So let's just go ahead and return every single result.
And we can just directly do return here. And then you don't need to mark this as asynchronous. There we go. So we just finished the entire router here. Let me just check if I have any unnecessary asyncs here.
I don't think I need async if I'm just directly returning. I could be wrong but I'm pretty sure you don't need it. It won't change anything if you do have it or don't have it, if the case is true that you don't need it. Let's remove these unused imports. Perfect.
And now let's go ahead inside of trpc routers underscore app and let's add credentials credentials router and make sure that you import it there we go Now that we have the credentials router, let me go ahead and mark that as completed. Now let's go ahead and implement the hooks. Once again, we can copy this from features, workflows, hooks. So I'm just going to go ahead and copy this and instead of credentials I'm going to paste it here. Let's start with use workflows.
I'm going to rename it to use credentials. Let's go inside of use credentials and now let's go ahead and just change things up. So hook to fetch all credentials using suspense, which will be called useSuspenseCredentials. For the params, yes, let's leave them like this now for now and let's just use trpc.credentials.getMany. There we go.
Hook to create a new credential. Use createCredential. And let's go ahead and do TRPC and let's create let's change both instances. So TRPC dot credentials dot create. This will be credential created failed to create credential.
Created, failed to create credential. Let's go ahead and replace this hook to remove a credential, use remove credential. And then again just replace these three instances with credentials, change this to be credential removed. Hook to fetch a single credential using suspense, So use suspense credential trpc.credentials like that. Hook to update a workflow name can be removed entirely and we can immediately go to a hook to update workflow, rename it to credential.
This will be use update credential, replace all of these instances to be credentials, change this to be credential saved And this to be failed to save credential. We don't need the execute hook at all and that is it. Great. A lot of you ask me whenever I do this kind of very similar code Why don't I just create an abstraction? Well, you absolutely could.
And we kind of did, you know, with entity components, right? We have entity item, we have entity list, right? This is an abstraction, right? But sometimes I just don't like them, especially for like hooks like this. This is a tutorial.
So obviously I'm making it kind of easier for myself by having both credentials and workflows have the exact same hooks. So obviously for you, it seems like I could have just created an abstraction here. But chances are in the real world in production your credential hooks and your workflow hooks will probably be a little bit different, right? So because of that I recommend not always rushing to create an abstraction. I think more often than not, it will lead to complicated code.
Sometimes having explicit separations is better in my opinion and I would rather have very similar repeated code than magic abstractions that are super hard to maintain and understand. So that's why. Great. One more thing we have to fix this is the use workflow params. So let's now go ahead instead of use workflow params Let's change it to use credentials params.
If it asks to update imports you can select yes and the only one it should update is this one instead of use credentials. Right here. So you can press save. It's still importing this, that's fine, we're gonna change that now. So we also need the actual params here.
So let me go ahead and copy that. Workflows params.ts Let me copy that file add it inside of the credentials folder. Page, page size, search. I think all of this is true but maybe we will also need a type. I don't know, let's leave it like this for now.
Great. So instead of useCredentialParams, this should now exist. There we go. But we have to rename them. So instead of credentialsFolder, params.ts, just change this to be credentials params.
And the answer, why don't I abstract this, is exactly the same, right? Again, I'm making this easier for myself and for you because this is a tutorial, right? But chances are you will have different query options for credentials than you would for workflows. And that's why I avoid creating this magic abstractions, right? It's completely fine to have similar code.
I really don't like obsessing with optimizing every single code repetition that you can find. I find it way easier to code in an environment like this than just having a billion magic abstractions. So let's go instead of useCredentials now And let's go ahead and replace this with, oh, I didn't rename it, my bad. Use credentials, params. There we go.
And I think this is the only place that we actually use that, yeah. Perfect. So now what I like to do is I like to right click on credentials, find in folder and let's search for workflow. Perfect. Workflows, nothing, which means we have very successfully created all the hooks required for this.
But I do think that we need to add one more hook here. And that will be to fetch credentials by type. And that should be quite easy. Let's go all the way to the bottom here and let's add it. So instead of use credentials, let's add a hook to fetch credentials by type.
So use credentials by type. Only prop it's going to accept is the credential type enum from generatePrisma. And then it will use useQuery. Let me just check. Is that the single query?
Why am I not having use query? Oh, yes, yes, yes, yes, yes. So this will not be use suspense query. This will, yes, just be a normal use query. I was surprised why don't I have used query already imported here but I forgot that I use use a suspense query where possible but use query is needed for this specific type because it will be used in a dialog.
It will not be able to be prefetched. I mean technically it's good but it's just simpler this way. Great. So that's it for hooks. Perfect.
Now we can go to the page, which is basically the server loader. So I'm going to go inside of source app folder, dashboard, rest, and we already have credentials. Perfect. Let's go inside of page.tsx here. So we already have require out.
The only thing we need to do now is we need to prefetch our credentials. We can do that by creating the params loader. So we have all the you know search filters pagination filters etc. And then prefetching those. So let's first define type props search params promise.
Let me add that here promise search params from nooks. Then let's go ahead down here. Let's destructure the search params. Great. So great beginning.
But now we have to go back inside of features, credentials, inside of server here, go ahead and create params.ts. Let's import createLoader from nux forward slash server and let's import credentials params from dot dot slash params and export const credentials params loader to be create loader and then pass in the params in here. So this is the exact same thing that we have in the workflows. If you go inside of server. Oh, so it's called params-loader.
Good idea. We should call it that. So let me go ahead inside of credentials and just rename this in the server to be params-loader. That's a better name. And then we also need to copy prefetch.ts.
So let's copy that and let's paste it inside of credentials server. Prefetch.ts. Now again, we're going to have to modify this a little bit, but it's just two of these. So instead of this being the input, it's going to be credentials.getMany, so prefetch all credentials, and this will be prefetch a single credential. So prefetch credentials, trpc.credentials.getMany, prefetch credential as in one, and trpc.credentials.getOne.
There we go. As simple as that. And I think that we are now ready to go back to our page.tsx here. Perfect. So now that we have that, let's go ahead and do const params await create my apologies credentials params loader search params there we go so credentials params loader from features credentials server params loader.
And then let's go ahead and just do prefetch credentials make sure multiple of them right from features credentials server prefetch and pass in the params. So this one prefetch credentials all of them right when you hover over this it should say prefetch all credentials. There we go. So now that we have this, we can go ahead and add the hydration boundary here. So hydrate client.
Let me just see which one do we have. Hydrate client from TRPC server. Error boundary from react error boundary fallback and let's just do error suspense which you can import from React with a fallback loading. And for now, let's just go ahead and do to do credentials list. All right.
So what I want to do is I just want to check if that's exactly what I do with the workflows. So workflows page dot DSX uses hydrate client which is exactly what I use here so hydrate client which is basically hydration boundary with the hydrate thing. Perfect. Great. Once we have this we are now ready to create the client side.
So we just finished this server loader. We now have to create the client hydration. So I'm going to go ahead and go back inside of my features folder credentials and I'm going to create components I believe or maybe is it you I folder first I'm not a hundred percent sure. I think it's just this yes And let's go ahead and do credentials.dsx. There we go.
And now I'm also going to open workflows, components, workflows.dsx and well I think that we can copy everything from here and just paste it inside and then we're going to work our way through refactoring this. So yes, we're not going to be using use create workflow or use remove workflow or use suspense workflows from this hook. Instead, when we are working inside of the credentials feature, our hook folder has these two. So let's change the import first. Use credentials.
Same thing for this. Use credentials params. Leave these to be errors for now. Let's just work through renaming the main things first. So this will be credentials, search, search credentials.
This will be workflows, not workflows list, it will be credentials list. Okay, let's leave this as is. This will be credentials, header. The title here can be credentials. Create and manage your credentials and this will be new credential.
Then for the pagination same thing so credentials pagination for the container same thing credentials container and now we can replace all these with credentials equivalent so credentials header search and pagination for the loading error and empty all same thing credentials loading error and empty, loading credentials. All right, empty view, you haven't created any credentials yet. Get started by creating your first credential. Workflow item will be credential item. We can leave this as, okay, So a lot of things changed.
So let's start with the simple change here. Let's find all instances that use use workflows params. So one, two, looks like three of them, right? Yes, three use workflows params. And let's change all of them to be use credentials params.
So I have changed this import, I have changed it inside of credentials search and I have changed it inside of credentials pagination. So if I search for use workflows params, I should find no results here and you shouldn't either. And now we're gonna do the same for use create workflow. So this one actually won't be needed at all here. You can remove use create workflow entirely.
Let's just find where it's used. Use create workflow. Yes, so credentials header will not have this at all. Let's remove it. And handle create will very simply just do router.push to credentials new because we're gonna need a form to create it And this is where the form will be rendered.
And the upgrade model will also not be needed here. So you can remove this, you can remove the fragment of wrapping it, And you can remove is creating. And now we want... Oh, we can actually... I think we can do new button href.
Yes. Credentials forward slash new. And then remove on new. And then remove this and remove this. Much simpler now.
That's why we've created this. So we have an option to choose whether we want to do a function or an HTTP redirect. Credentials pageant. Okay, let's go back here. Do we still need use workflow, use router?
We do. Let's now find a use remove workflow. I think these are only two instances. Perfect. So instead of credential item, you should rename it from the input from the import and here to use remove.
I have no idea how I changed that so badly. So use remove credential. There we go. Use remove credential. Instead of credential item, let's now call this remove credential to the href should go to forward slash credentials and image here it can just be key.
I don't know let's just leave it to be this and let's just use remove credential is pending. Leave this to be workflow. We're going to handle the details later. Now we have use suspense workflows and I think this is also used in not too many places, so just three places and let's change it to be use Suspense Credentials and I click this to replace all. So let's see exactly where.
The first place is here in the import. The second place is in the credentials list. The third place is in the credentials pagination. Make sure you have changed all three to use suspense credentials. Perfect.
Now let's fix one by one. Instead of credentials list, instead of entity list here, first things first, This is now credentials. So let's change that. This, this, this and this is all a single credential. We no longer have workflow item nor workflow is empty.
So let's just do credential item and credentials empty. There we go. So no more errors in the import. That's great. Let's scroll a bit down to see what's going on here.
Instead of credentials empty we still have create workflow. I see handle create here. So on new let me see empty view only accepts on new. Got it. So we're just going to go ahead and use the router push here.
So instead of handle create, router push to credentials and then just to new. We can then remove this, we can remove upgrade model, we can remove the fragment and everything and make it just that much simpler. Perfect. I think that's a lot of things resolved. We can now remove useUpgradeModel import from here.
So now what I want to do is I want to change the icon. Yes. So, the icon, let me try and find a nice way to do this here. In fact, I think it's time to render this so you can actually see what we're doing because we just changed a lot of code. But it's all code we've seen before so we know how this looks, right?
So let's just go ahead and go back inside of our dashboard, Rest credentials page.tsx and let's render the credentials list. From features, credentials, components, credentials. And I think that now finally if you go ahead and refresh this and click on credentials, you should see a very similar look, but it should say no items, you haven't created any credentials yet. Get started by creating your first credential and if I click add item it should redirect me to credential ID new which is technically correct but obviously we will change that later but at least the redirect is working. Great.
So now also what we have to do is we have to go inside of credential, my apologies, inside of page here and we have to add the credentials container like this. And let me just see. So credentials container. You need to import that from the same list where you've imported credentials list from features credentials components credentials. So credentials container simply has the header search and the pagination and some additional styling to make this centered.
There we go. So it was that easy for us to create the same layout that we have in workflows and that's why I didn't render it until now because it is exactly the same. It's nothing you haven't seen before. But yes, I think now it might be time to start seeing what we actually changed code for so you can start to notice if there are any bugs so you have time to fix it. Because What we need to develop now is this, the new page, right?
So we already have, the reason this is not showing 404 is because it is going to slash credentials slash new. And if you take a look here in the dashboard. We have that. That is this. But that's not exactly what we want.
So we can very simply override. I mean like allow every single credential ID to be loaded here except new by creating a new folder and literally calling it new. And now if you go ahead and create a page.tsx here and go ahead and just div form to create new. You will see that now that's what's rendered here because my current URL is the following and yours should be too. So this is my current URL forward slash credentials slash new.
But if I change this to one two three and go here then you will see the difference right now I'm using the other folder. I hope you understand right. So if we hit anything that isn't keyword new it's going to be using this dynamic loader to load the ID of the credential. But if I literally type in for forward slash new, it will redirect to this page right here. And this is our chance to build the form to allow the user to create a new credential.
So let's go ahead and just go back inside of credentials page dot TSX And let's go ahead and use our credentials error component here so we don't forget that. And let's use our credentials loading here so we don't forget that either. So now if I go back inside of credentials itself, it should have just a tiny bit nicer experience for the loading and for the error if it happens. And now we can go ahead and entirely focus on the new page. So this will be an asynchronous server component and first thing we're going to do is we're going to require auth so it redirects the user if they are not logged in.
Then let's go ahead and give it some styling here. So for this div I'm going to give it a class name of padding for medium of px10. My apologies, on medium breakpoint give it a px of 10 and on medium give it a py of 6 and full height. And then we're just going to go ahead and kind of limit the maximum width that our form container will be available to be in. So that is this div right here.
I'm going to go through the classes now, don't worry. So div class name MX auto max with the screen MD full with flex flex column gap y 8 and height full. Now let's go ahead and render credential form. Since this does not exist, naturally it's going to throw an error. Now let's go ahead inside of features credentials components.
Let's create a new file credential.tsx so a single one not multiple ones. Let's mark this as use client and let's start by creating an interface credential form props. Initial data that will be accepted will be an optional ID because This can either be used as a new credential form or as an update credential. And besides the ID we're going to have a regular name, type which is a type of credential type, credential type from generated Prisma, and value which will be another string. Great.
Now let's export const credential form. Now let's assign the credential form props here. Let's go ahead and destructure the initial data inside. Great. Now let's go ahead and let's prepare some hooks.
So we're going to need four of them. Router from useRouter, which we can import from next navigation. UseCreateCredential, which we can import from dot dot hooks, useCredentials. UseUpdateCredential from the exact same place and then finally the premium one use upgrade model. So this is much easier for us because we've already created all of these components before.
Let's define if this will be editing or creating a new credential by very simply checking if we have initial data question mark id. Then let's go ahead and define the form. The form will be quite easy. Const form will use useForm from React hook form. It will use form values which I forgot to implement so let's leave it empty for now.
It will use ZodResolver from hook form resolvers Zod. So these two are the ones we've added. And now in order to add form schema and form values, we're going to have to add a Zod and define the schema. The default values will either use the initial data prop or they're going to fall back to empty name, empty value and a default type of OpenAI. Now let's go ahead and let's create the form schema.
So the form schema will have a name, type and a value. We can import Z from Zod. Name will be a string value will be a string and type will be an enum of credential type. And let's go ahead and do type form values here z.infer type of form schema. There we go.
We should now have all of those errors completely resolved. Perfect. Now let's go ahead and let me create one factory map here to properly load the logo of each of our providers. So const credential type options and now let's go ahead and create value credential type.openai label W credential type that open a I label open a I logo forward slash logos open a I dot SVG let's copy them twice Let's change this one to be and tropic and this one to be Gemini. So let's change the label and the logo accordingly.
There we go. Now that we have that factory here, we can go ahead and start building our forms. So let me go ahead and just add some other imports that we're going to need, starting with all the necessary form components. Form, Form Control, Form Field, Item Label, and Form Message. Then we're going to need the import, my apologies the input.
Then we're going to need Select, Select Content, Item Trigger, and Value. And let me see, I think we might be... Let's go ahead and also add instead of use credentials use suspense credential here because if this is an update we will be we will need to load it. I think. OK.
Now let's also go ahead and let's add everything we need from a card card content description header and title. And let's go ahead and let's add button. And let's go ahead and let's add. Do we need a review like this. Yeah let's not add this.
We're good without that. And instead of next navigation here let's also add use params and let's also import image from next image. I think that's all the imports resolved now. Now we can go ahead and build in peace. So I'm going to go ahead and start with the return here.
Card. Class name shadow. None. Now let's go ahead and add card header. Card title and we're going to check is this an edit in that case edit credential otherwise create credential and we're just going to do the same thing for other things now for example description Either update your API key or add a new API key.
Then let's add card content here. Let's render the form. Let's go ahead and spread the form constant. Let's render a native form element on submit for now let's do form handle submit like this. Let's give it a class name space y 6.
You can leave the error as is for now and let's go ahead and render this form field with control form dot control name of name render off field form item form label let me fix the typo here name form control input placeholder my API key, and spread the field property. So let me go ahead and try and zoom out a bit so you can see how it looks in one line. Then add form message which is a self-closing tag which will display any errors if they appear. In order to fix the handle submit we have to implement it so I'm just going to do const on submit here asynchronous values type of form values. Let's check if this is edit and if we have initial data question mark ID await update credential dot mutate asynchronous with an ID of initial data dot ID and simply spread the new values.
Else let's go ahead and do await createCredential.mutateAsync passing the values and let's go ahead and do onError here grab the error handleError and pass it along. So what is handle error? We have it here in useUpgradeModel So what we also have to do is mark this entire thing in a fragment, like so, and render a model inside. Now we can quickly go back inside of page new and just import the credential form. From features, credentials, components, credential.
And you can already see it start to form. Create credential, add a new API key. Perfect. Let's go inside of credential here and let's continue developing this. So passing on submit here.
Now besides that single form field let's go ahead and add a new one. So form field again. We can go ahead and copy these two. This will be now for the type. We can copy the render as well, but the content inside will be slightly different because this will be a select.
So we start with form item, form label, type and then we add the select component. The select component will have two fields on value change and default value. In here let's add form control. Let's add select trigger. Let's give it a class name of full width.
Whoops. Inside of select trigger let's render a select value with a self-closing tag. Outside of form control, let's add select content and in here let's do credential type options dot map. Let's get an individual option here. Render select item here.
And let's pass in the key to be option dot value and value to be the very same thing. And now inside of here let's do a div with a class name flex-items-center-gap-to and render an image. The image should have source option.logo alt option.label width 16, height 16 and render option.label There we go. And now here just add form message. There we go.
And finally one more thing that we need is a super simple form field. So after this one, let's just go ahead and add this. So the same as the first one, a super simple form field which controls the value prop and it uses the API key as the label and inside of form control it very simply renders an input with a type of password, a placeholder with something to tell the user that this is supposed to be the API key and it spreads the field property here. There we go. And it renders the form message of course.
Now just before we end the form we should also render the submit buttons. So let's create flex gap for button type submit disabled will be either if create credential is pending or if update credential is pending then let's go ahead and very simply choose what to render inside. If it's edit then update otherwise create And finally, another button next to it with a type of button variant outline which on click will simply redirect to credentials. I think we can even make this simpler, maybe using next link, adding an href to credentials, prefetch to make it faster, remove on click and add as child. I think that's a better practice to do.
Perfect. I think that's it. So let me check. Did I need use params at all? We are going to need it later but not now, not just yet.
So let's check if this works. Inside of credentials right now I have no items and if I click add item, test credential, type OpenAI test, let's click create, create test credential created. So if you want to, you can redirect the user to that newly created credential. So you could go on success here. Data, router.push, credentials, data ID.
You could do that. You could go back to the list of all of your credentials, whatever you think is a better user experience. But if you now go back to credentials, you should see your new test credential right here. Let's test if delete is working. There we go.
That seems to be working and now we're going to test if my redirect is working. There we go. That is working too. Great. Now let's go ahead and let's fix which icon shows here.
So in order to render the proper image we have to go back to credentials.tsx right here and we are very simply going to go and find the credentials list. More specifically maybe credential item. And just above this, let's create a super simple factory credential logos, which will be a type of record, which The key is going to be a credential type which we can import from generated Prisma. And I'm just going to go ahead and do. Oh, I'm still using this.
Okay, still some leftover workflow thing is here. Let's fix this like the following. Leave that to be a type import and this will be a normal one. So then you will be able to use the credential type here for the factory. Just make sure this matches of course your public folder.
And once you have the credential logos here, let's go ahead and do the following. Const logo will be credential logos, data.type, which of course we have to modify here because it's no longer going to be a type of workflow or fall back to logos open AI dot SVG. And let's go ahead and render that so inside of the image let's just go ahead and use the image itself. There we go. Image, source logo, alt, data.type with 20 height 20.
Let's see what do we have to fix here. Element implicitly has any type. Okay. Something is wrong here. Let's start with giving it a proper type.
So this should be a type of credential. But I think credential it might already be taken so let's import type credential. Oh I already have it. Okay. Credential.
But I'm not sure if credential itself might be taken as a constant somewhere. Looks like it is not. So make sure your credential item uses data with credential as the type. And now let's see what the problem is here. Probably because I need to import image from next image and we can remove the workflow icon And there we go.
Now it will basically display exactly what type of credential it is. So if I create a new credential with OpenAI type and if I go back to credentials It should render that. Let's go ahead and search for something. There we go. That works too.
Perfect. Let's go ahead and try Gemini now. Let's click create. Back to credentials. There we go.
Gemini. Perfect. So now we have to create this page right here which will be super simple, don't worry, because we already implemented every single thing that we need for this. We just have to go back to credential.dsx, so a single one where the form is, and go all the way down here, export const credential view, const params, use params, const credential id, params.credential const credential ID params dot credential ID as string use suspense credential passing the credential ID destructure the data rename it to credential and return credential form. But this time give it some initial data.
This way credential form will be rendered as an edit form. Be super careful not to misspell credential ID. So params.credentialID needs to be exactly as you have written it here credential ID. Capitalization matters. Triple check that it works.
Now that you have the credential view here let's go ahead and go inside of our credential ID page dot TSX and in here we should obviously render the credential view like this. So let's go ahead and import it. Credential view. And now that I think of it, Why don't I just accept the credential ID prop and use it? I think that's much simpler.
And then I can just use the loader to pass in the credential ID. Yeah, that's much simpler. We can just do that and then we don't need use params. Perfect. Now let's go back and set up the page here.
What we're going to have to do now is we have to prefetch it. So prefetch credential, pass in the credential ID, Make sure to import prefetch credential from features credentials server prefetch. So prefetch a single credential. It only accepts the ID. Perfect.
And then in here let's go ahead and just do some styling. So the styling will actually be I think the very same like this. We can copy this entire thing, paste it like this, add two closing divs, and then let's go ahead and let's add the hydration client. You can import this from the RPC server. Then the error boundary from react error boundary For the fallback here.
I'm just going to reuse my credentials error So yes, I'm going to reuse the one from the credentials not the one from my apologies from components credentials, even though I'm mostly using the ones from credential simply because I have no idea how differently I would create the one for a single credential. You can import suspense from react. And you can also pass in the fallback here to be credentials loading. So same import from the credentials component. All right.
I think this should now work. Let me go ahead and just try. So let me zoom out just a bit. Oh, something's not good here. Let me see this issue.
Okay, that's something else. But instead of my credentials now, if I go ahead and click here, There we go. And if I change this to Anthropic API like this and click update and go back to credentials. There we go. Updated less than a minute ago, but created nine minutes ago, Anthropic API.
Perfect. It is officially working. We have the entire entity structure for credentials. What we can do now is We can go ahead and implement that so that here instead of our AI nodes we have a drop-down which will allow us to select any of the credentials that we have. So Let's go back to one of the dialogues.
I think the easiest one to try out is the Gemini one. So I'm just going to go ahead and add the Gemini block and I'm going to click save here. I'm going to go ahead and keep it open just so I can see what I'm developing. Then I'm going to go ahead and close all of this code. I'm going to go inside of source, features, executions, components, Gemini, dialog.dsx.
In here I'm going to go inside of the form schema And I'm going to add the credential ID. So after a variable name, I'm going to add credential ID with a message, credential is required. Then I'm going to go ahead and make sure that I have it instead of my default values here so credential ID is going to be default values dot credential ID or an empty string. I'm going to copy that and I'm going to do the same thing in the form reset here. Now that we have that we're going to have to create the drop down for our credentials.
But this will actually be a little bit easier because we can use our use credentials by type hook. So right here, I'm gonna do const useCredentialsByType. And I'm going to select credential type from generatedPrisma.gemini. So make sure you have imported credential type from generated Prisma and use credentials by type from our features credentials hooks use credentials. From here we can destructure the data and we can alias it to credentials.
And let's also get is loading here. And let's alias that is loading credentials. Now let's go ahead and go down here and what we're going to do is we're going to develop a new form field. So if we have any existing select ones That would be great so we save some time. So I think we have one inside of our credential.dsx.
So inside of credentials folder credential.dsx we should have select somewhere. Perfect. So find the name type, form label type, copy the entire form field here. And now let's go ahead and add it after a variable name. So just add the entire thing here.
And now we are going to resolve the errors. So the errors are mostly missing components. So we can just add select select content item trigger and value. There we go. Now we can go here and we can start to fix these.
So this should control the credential ID because that's the new one we've just added. So this will be Gemini credential like that. And it will be disabled if is loading credentials or if we have no credentials at all. So if there is no credentials length And now let's go ahead and give this a placeholder. Select a credential.
And now instead of the select content we're going to do credentials question mark dot map and this will be key dot ID. This will be my apologies. Key will be option ID value will be option ID as well. So make sure you are using ID here and source will actually be hard coded to logos Gemini SVG. Alt will be Gemini.
Option here will be credential or option dot name. Let me rename the option thingy to credential and make sure to import image from next image. There we go. So just like that you have added a new property to the dialog. I'm going to go ahead and open this and there we go.
I can now select a credential that I want to use with Gemini. Perfect. But we're not done yet. What we have to do is we have to visit the executor. So inside of features, executions, components, Gemini, executor.ts.
Let's go ahead and first things first extend this with a credential ID and then we can go ahead and check if we have it or if we don't have it and we can throw an error immediately. So just as we check if we don't have the variable name or the user prompt we can also check if we don't have credential ID. Gemini node credential is required. Immediately throw a non-retriable error and publish an error status. So how do we fetch the credential?
Well, we can now remove this to do because we already do that and now this is where the magic happens. Const credential is going to be await step dot run get dash credential then let's return prisma which we have to import from lib database dot credential dot find unique where passing the id data dot credential id if there is no credential throw new new non-retriable error GeminiNode credential not found. Now we can remove this credential value and we can just use credential.value from here. Now there is a question should we just fetch this you know without user ID. Well the thing is this isn't exactly a public API.
This is a background job within ingest. So even if we fetch this credential, we are not returning its value anywhere. We are just fetching it. Imagine this is a webhook call. We also wouldn't exactly know if it was a user who executed this or something else, right.
So I think this should be completely fine to do and we don't need user ID here to fetch for the security role level. And in here we check if we don't have it and we throw the error perfect. So let's try running it exactly like this. I'm going to go ahead and call this my Gemini. Credential will be this one.
Make sure you have at least one Gemini credential. Test. Hi there, how are you? Let's click save. Let's click save here.
Let's go ahead and execute workflow and what I expect is for the workflow to fail because of an invalid API key. Exactly. Please pass a valid API key. So now I'm going to go inside of my credentials here inside of this one. I will change this to be Gemini personal.
Gemini personal. Like this. And let's go ahead and go inside of dot environment here and let me copy this one and let me paste here And click update. And then in workflows, let me just check if, you know, the name was maybe updated. It's still reflecting this.
I think I have to refresh maybe. Yes, because I don't like it's now. Yeah. After a refresh, it will appear because we don't invalidate this one after updating. Maybe we could do that, but you don't have to update anything here.
You can just execute again. You don't even have to save it, but it should work this time. Get credential. There we go. Successfully completed.
We successfully implemented Bring Your Own Keys. Users can now create their own credentials and pass them along. You can finally remove this from your code. I would suggest you keep it till the end of the tutorial because I forget my credentials so many times and I accidentally delete it while testing. So yeah, you can, but I would suggest still keeping it here.
But finally, your users can now add their own credentials. So just one thing I want to check. Inside of source features credentials. I'm just going to do one find in folder and search for workflow. All right.
Inside of credentials.dsx I'm still calling these workflows. So credentials pagination. Let me show you what file it is. Credentials folder, components, credentials.dsx. I'm going to change all four instances to credentials.
There we go. And I think that now if I go ahead and right click find in folder, workflow, workflows, nothing. Successfully migrated. Great. So what we need to do to finish this chapter is do exactly what we just did for Gemini, right?
Let me just check if that's all we need. Instead of node.tsx I should also have credential ID here. There we go. So now I have the proper types everywhere. Now let's go ahead and implement the same exact thing but for Anthropic.
So I'm going to go inside of Anthropic, node.tsx, I'm going to copy credential ID right here and I'm just going to paste it here. Perfect. I'm going to close node in both of them. I will open Gemini dialog and I will open Anthropic dialog. I will go inside of Gemini 1 and I'm going to copy the entire form field here with the select option to load the credentials and I'm going to add that exactly in the same place.
So after variable name. Of course it will be riddled with errors because we don't have many of the imports we need. But we are going to resolve that now. Now I'm just going to go ahead right here and I'm going to import image, credential type and use credentials by type. So all of these imports and I'm going to add them to the Anthropic block.
Now let's go ahead and go back inside of the Gemini one And let's go ahead and actually load credentials by type. So I'm going to do the same thing here now. There we go. And a few things left to do is to modify the schema. So copy credential ID.
Let's go ahead and paste it in Anthropic node and already you can see no more errors left but there are a couple of more things the default values so let's just go ahead and add that default values, perfect and form reset perfect let's check if we did this correctly inside of workflows right here I'm going to delete this node and I'm going to add entropic node like this well I can just open and see the problem is it's fetching Gemini once so let's go inside of Entropic node and let's just make sure that when we fetch them we are using credential type.entropic this time. So now it should only fetch Entropic API which also means we have to modify this logo to entropic.svg and entropic as the alt. There we go. Now we can select our anthropic API. Perfect.
We can now close both dialogues and let's instead open both executors. So let's go ahead and go inside of Gemini executor and let's copy the credential ID and let's add it to the Anthropic data after variable name. Then let's go ahead and throw an error if credential ID does not exist. So after variable name inside of anthropic go ahead and throw the error just make sure that you're throwing it for the anthropic channel. There we go.
Change this from Gemini node to Anthropic node and looks like I accidentally left over open AI thing is here so feel free to fix that as well if you want to. We can now remove this to do Then let's go ahead instead of Gemini executor and let's go ahead and copy these two things basically an option to fetch the credential so we can now remove this part. Paste it here. We have to import Prisma from lib database. Anthropic node credential not found.
You can remove the credential value and you can use credential.value this time. There we go and I think that's it just make sure you've imported Prisma make sure you're using the Entropic channel. Great. So this should now work. Now I'm going to go ahead and do the exact same thing but for OpenAI.
And looks like we forgot the title here so it still says Gemini credential. So dialogue.tsx in the Anthropic folder. It should not say Gemini credential. It should say Anthropic credential. Great.
So I'm going to go ahead and do the same thing for OpenAI by going inside of node.tsx and I'm simply going to add credential ID string. That's it. Then I'm going to open the dialog for OpenAI and I'm going to open the dialog for Gemini. This time I'm going to start by adding all the necessary imports. So image select credential type and use credentials by type.
And I'm just going to go ahead and add all of them inside of the OpenAI dialog.tsx. Then I'm going to go ahead and modify the form schema by adding the credential ID after the variable name. Then I'm going to go ahead and fetch the credentials. I'm going to now add this to the open AI dialog. There we go.
Now I'm going to go ahead and make sure the default values load the credential ID. And the reset form loads it as well. There we go. And now I'm going to go ahead and I'm going to copy the entire form field which renders the credentials. So I'm going to copy that entirely.
I'm going to find the form field after a variable name and I'm going to just paste everything here. I'm going to fix the indentation and since we already added all the imports above, there are not many errors. Let's just change this to OpenAICredential, select a credential, logos OpenAI SVG, OpenAI. Perfect. So let me go ahead and test this out.
I'm going to add an OpenAI node. There we go. But it's still fetching Gemini ones. I forgot to change that. Credential type dot OpenAI.
Make sure you're doing this instead of open AI dialogue instead of open AI folder. And you can see that now I have only one result right here. So naturally if you didn't have any open AI credentials you would simply have an error. I mean not an error you just wouldn't be able to fetch any so just make sure you have at least one of each so you can test properly. And now what we have to do, the dialog is finished.
Let's open the executor for OpenAI and let's open the executor for Gemini to wrap this chapter up. So I'm going to close both dialog components now. I'm going to start with the imports here so import Prisma. Let me add that here. Then I'm going to go ahead and add the credential ID to my OpenAI data.
Then I'm going to go ahead and throw an error if data credential ID is unavailable. There we go. I'm going to make sure that I'm using the OpenAI channel to emit that news to the frontend. Then I'm going to go ahead and fetch the credential and throw if it is not found. So I can now remove this to do and paste that here I can remove the to do here because I'm throwing now.
Let's go ahead and change this to be open AI node credential not found. Finally I can remove this and I can use credential.value. There we go. I think that is it. That's all we have to do.
So we can now go ahead and close everything here. We successfully implemented the entire thing we outlined here. We added client, we added entity components for pagination, for search, for other things, and we even added credential drop down and we even added it to the executor so we have a proper bring your own keys method. Now let's go ahead and push this to GitHub. So 25 credentials.
I'm going to go ahead and create a new branch. 25 credentials. I'm going to go ahead and stage all of my changes, 25 credentials. I am going to commit and I am going to publish this branch. And now let's go ahead and let's review our pull request.
Since this was a large one I'm gonna go ahead and let CodeRabbit do it for us. And here we have the summary by CodeRabbit. New features. We added credential management system for storing API keys for an OpenAI, Anthropic, and Gemini. Users can create, view, edit, and delete credentials through a new management interface.
We added search and pagination for credentials list. AI nodes now support selecting stored credentials instead of using environment variables. So exactly what was the goal of this chapter to allow users to bring their own API keys. Now in here I think the thing we should focus on the most is all of these comments that CodeRabbit left. So 14 actionable comments.
Of course, you can pause the screen right here. I always think these are super useful if you are struggling to understand how our code is working. So using this sequence diagrams you can pause the screen and you will figure out exactly what happens when a credential is not found when a credential is found etc. So let's go ahead and maybe we can read through this here. So executor integrations.
First verify that credential fetching, validation and error handling are consistent across OpenAI Anthropic and Gemini. Ensure credential ID is properly threaded from dialog, node, and executor. TRPC router security. Confirm that all endpoints properly enforce context out user ID to prevent users from accessing modifying credentials belonging to others. Database schema integrity, verify foreign key cascading behavior, cascade on user deletion, set null on credential.
Yes, this is, if you remember, this is what we, exactly what we discussed when we started developing. That's why I wasn't sure, but even CodeRabbit says that that's what would be the correct choice here. Set null on Cascade. Otherwise you will not be able to delete a credential which is assigned to a node or vice versa. So because of that set null will be the correct option.
So yes, we could definitely add this in the next chapter. And form validation and error handling here. So let's take a look at the comments here. So some things are not true simply because there are newer versions of Zod than for what AIs were trained on. For example, native enum is as far as I know deprecated and you should use enum now so this is correct and it works correct for us so we don't have to switch to native enum here at all.
In here it's telling us that we have different types of different invalidation patterns, somewhere we are using query filters, somewhere we are using query options. I think this is because that's the demonstration I found on TRPC website. So that's why in validation I'm using query filter, but in fetching I'm using query options. I'm not really sure that it matters. I think most will work.
And yes, we forgot to throw any toast errors here on error. We could add that, sure, like this. Then let's go ahead about this. I think this is the same thing. Z.enum is fully supported and it works and of course it is addressing the security vulnerability.
Our values are stored in plain text and in here it is suggesting using a library like this to encrypt it, which is interesting. I have not looked into this library right here. Perhaps you could look into it and see if that is something that would be a good idea for your project. Now let's go ahead and I think the same thing is here, native enum, so completely supported in our case, Same security vulnerability API key stored in plain text. Now let's go ahead and okay again, native enum.
In here I think again, yes, it suggests using value instead of default value. I think I'm using the example from a chat CN documentation, so it works, I'm not going to change it. And this is important. I think I had a whole monologue here about how we don't need to check if the tenant is allowed to access this credential. But what I forgot is ID injection, basically using someone else's credential.
I completely forgot about that. So yes, we should definitely also add a way to validate if this user is allowed to fetch that credential. So I was only thinking about one side being secure, but I forgot that the user can be malicious as well. So yes, very, very good catch here. I did not think about IID injection.
We should find a way to always verify what user is triggering the workflow and then confirm whether user is allowed to fetch the credential or not. Then in Anthropic Executor, If I'm missing a credential, I throw the non-retriable error, but I forgot to publish the status for the error it seems. And I think I also forget to do the same in Gemini. So same problem in all three obviously. User ID context and I throw this without doing publish.
So yes I should probably update all of that and then just repeated comments for this case. Same thing maybe some incorrect providers. It looks I'm writing Gemini inside of OpenAI. So yes, small typos here and there, missing status publishes. Yeah, we should definitely do this to give users a better experience.
And in here, yes, It's basically telling us that we should consider encrypting our API keys. So what I did in my previous project was I used AWS encryption actually. You can also do it yourself using database level encryption. But using AWS Secrets Manager is actually surprisingly easy. The hardest part is configuring the IAM profiles.
So yeah, consider researching into AWS Secrets Manager or the open source package it recommended above. Great great comments. I will mostly focus on fixing the ID injection in the next chapter and fixing the missing channel updates. So for now let's go ahead and go back into the main branch. Let's go ahead and click synchronize changes and let's click OK.
Then let's go ahead and open our graph to make sure we have merged everything. Perfect. That seems to be correct. A lot of new things we've learned today. A lot of things we've learned what we should do once we go into production as well, right?
You can see how the sentiment around storing plain text API keys is, right? So it is considered a security risk of course. Then again you will meet some people who will say that they can be easily rolled. I wouldn't consider having to encrypt them. But I think users are trusting you with their data.
You should find a way to encrypt them. I will try my best at the end of the tutorial to give you some proper recommendations about how to do this. And again, you can literally use my previous tutorial and it shows exactly how to do it. It will depend on how much time we have for this one. Amazing amazing job and see you in the next chapter.