-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ Refactor ] Explicit CRUD Operations and Expiration Integration #37
Conversation
I will look into this as soon as I get half an hour or so to deeply check the implementation, @mw10013! I'm not sure if it will be today or tomorrow, but I promise I will look into it! Also, I will invite you to Otherwise, feel free to create a npm package and call it whatever you want, in case you want to test things on your own. Until I get the time to properly look into this and set up the Once again, thank you so much for the time and effort you are putting into this!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic work, @mw10013! A similar approach has been taken by Kent C. Dodds on the Epic Stack with the @epic-web/totp
package.
In this case, I wanted to keep the extra logic for us, the maintainers, and give the developers a simpler API: handleTotp
, which was managing the read and update, if I'm not mistaken.
Simplifying it is a great choice and one that should have been made from the start. In any case, I'm happy to be at this point.
I will have to review the code on my editor, to ensure that Prettier is satisfied, imports are well situated, and some minor details are addressed, which are purely personal preferences. As the project grows and more people start handling it, these preferences will gradually disappear.
A migration guide would be great, and any documentation updates as well. I will also revisit them, so as long as you add the basics or whatever you think the end user needs to understand, that will be great.
I usually have the help of ChatGPT for docs, which can simplify them and make our writing easier to understand for everyone, so don't worry too much about the docs.
I will approve this and will wait for you to comment back on anything you think is missing or to move on to the next steps.
Thank you so much for this fantastic work!
src/index.ts
Outdated
@@ -12,6 +12,29 @@ import { | |||
verifyJWT, | |||
} from './utils.js' | |||
import { STRATEGY_NAME, FORM_FIELDS, SESSION_KEYS, ERRORS } from './constants.js' | |||
import { CreateContextOptions } from 'vm' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is CreateContextOptions
used? ESLint indicates that it is defined but never used.
I'm at the beginning of the review, so I'm not sure yet, but I'm leaving this comment here in case we need to revisit it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be removed and will do so in the revision to this PR
*/ | ||
attempts: number | ||
export interface CreateTOTP { | ||
(data: TOTPData, expiresAt: Date): Promise<void> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, should expiresAt
be a Date, and only a Date?
Could this be an inconvenience for some databases, or should it be suitable for most of them?
I haven't reached the implementation yet; I'm simply commenting on things I find interesting and that could be great for discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Date should be suitable since it includes time. Note that an ORM like Prisma has way to map Date's to database types and vice versa.
https://www.prisma.io/docs/orm/reference/prisma-schema-reference#datetimeDateTime
Remarks
Prisma Client returns all DateTime as native Date objects.
Connector Default mapping
PostgreSQL timestamp(3)
SQL Server datetime2
MySQL DATETIME(3)
MongoDB Timestamp
SQLite NUMERIC
CockroachDB TIMESTAMP
src/index.ts
Outdated
private async _storeTOTP(totp: StoreTOTPOptions) { | ||
await this.storeTOTP(totp) | ||
} | ||
|
||
private async _sendTOTP(data: SendTOTPOptions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some private _methodName
simply does what the original method passed to the strategy does. I encapsulated them into private _methodName
in case we need to handle specific logic in the future, but we could remove some private _method
names if they do not add value.
I added a few private _methodName
methods that were not handling any extra logic, simply for consistency. I think I will end up refactoring this and simply call the original method passed to the strategy, unless we have to handle any extra logic, in which case it will be encapsulated as private
.
I'll keep it like this for now, but revisiting it in the future would certainly be great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to remove it in the next revision to this PR.
throw new Error(this.customErrors.invalidTotp) | ||
} | ||
} | ||
|
||
private async _handleExpiresAt( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to have this out of the code; it was somewhat of a headache to come up with a solution that could be beneficial in most cases, based on the provided user database.
I'm not sure if it was the best implementation, but it did its job in the meantime.
|
||
const request = new Request(`${HOST_URL}`, { | ||
method: 'POST', | ||
headers: { | ||
cookie: await sessionStorage.commitSession(session), | ||
}, | ||
body: formData, | ||
body: new FormData(), // Empty form data indicates re-send new TOTP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's me who's learning, @mw10013! I'm happy about it!
@@ -723,6 +842,116 @@ describe('[ TOTP ]', () => { | |||
expect(session.data.user.name).toBe('John Doe') | |||
}) | |||
}) | |||
|
|||
describe('End to End', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this! Also we required an End to End test that could help other developers understand the flow of the strategy just by looking at it!
test/index.spec.ts
Outdated
@@ -14,14 +14,16 @@ import { | |||
DEFAULT_EMAIL, | |||
sessionStorage, | |||
} from './utils' | |||
import { Session } from '@remix-run/server-runtime' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Session
used only as a type? If so, it should be imported as a type, and I think it wasn't. No worries though, I will look into it right now.
Also, are SendTOTPOptions
, TOTPData
, and TOTPStrategy
used as types too, right?
I think they were not imported as types before, right? In that case, it would be my mistake, or I'm probably being wrong about it. (I'll have to revisit the code in my editor, that would help me a bit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Session
, SendTOTPOptions
, and TOTPData
are types and I'll import them as such in the next revision. TOTPStrategy
is a class.
@dev-xo: Revised PR based on your review, update docs, and added migration guide. Please review and let me know how you want to proceed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lovely! Thank you so much for all the care you've put into this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing!
This was a fantastic rework, @mw10013! You also made it look so simple. I want to review the project in my editor, and after that, I think we could push the npm package to v2, create the Release, etc. Not sure if you had the chance to test it on any of the templates. If you are up for it, I could push Let me know about that. |
@dev-xo: If you are able to push |
Sounds good to me, @mw10013! I will. I also need to update the other templates and the main |
Mostly personal preferences for imports, comments, etc. Nothing important.
Not sure if we have to discuss anything more, but I will wait a bit in case you want to try the package on the starter template. P.S: I was trying to reply to you on Discord, but I'm getting this message: "Your message could not be delivered. This is usually because you don't share a server with the recipient or the recipient is only accepting direct messages from friends." I think we both share the Remix server, and I also have you added as a friend. Again, thanks a lot for this fantastic work! |
@dev-xo: I've added more tweaks to the docs in this PR based on testing |
try { | ||
// Delete expired TOTP records. | ||
// Better if this were in scheduled task. | ||
await prisma.totp.deleteMany({ where: { expiresAt: { lt: new Date() } } }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh, I'm not sure about this. If a database contains a large number of expired TOTPs, this step could take more time than necessary; the authentication phase will feel slow.. Also, if the removal of expired TOTPs fails, the authentication flow will fail, or am I wrong about this? (I'm probably wrong. The creation of TOTP is handled beforehand, although I still think this could be implemented somehow by the user.)
Should we document this somewhere else, instead of adding it directly to the authentication/creation of TOTP phase? Although it could work, I'm not sure if it would be the best move.
Another option could be to remove the await
from the deleteMany
call, right? The database would still handle that without blocking the authentication flow. Anyway, I think it's okay for now, and it could also provide a hint to other users on how to delete expired TOTPs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For someone who knows what they are doing and are building an application to scale. They would never do this and would put it in a scheduled task or some other mechanism outside of the createTOTP
.
For the beginner or someone who is in a hurry just to get something working and doesn't need to scale, this gets them going without filling up their database with expired TOTP's. With an index on expiresAt
, this should not be an expensive operation especially at small scale.
The try/catch around it should prevent any delete errors from causing createTOTP
to fail. Removing the await
is very interesting idea.
prisma.totp.deleteMany({where: { expiresAt: { lt: new Date() } } }).catch((reason) => console.warning(reason))
The code starts to look a little more obscure to the beginner who may be more comfortable with await
.
Based on your feedback, I think we should remove expiresAt
from the README code and have an expiresAt
section in Customization. I can make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think you are absolutely right. For the beginner or someone in a hurry, this is just fine, and it also gives the idea that it could be handled differently later at some point.
Nothing to change here, everything is just as good as it can be, @mw10013, and you thought about it very well and carefully!
Also, let's keep the await
call in there, it's the most common way to call async functions and removing it could be somewhat a visual disruption for folks in a hurry to learn the basics of the Strategy.
Alright, with everything looking this good, I will look into creating the release on GitHub.
As I said before, a million thanks for the amazing work you've been doing, testing, etc.
Merry Christmas to you!
Amazing, so V2 is ready! I will do the required steps later today, and by tomorrow we should have it operational. Again, many thanks for the mind-blowing efforts and the time invested in this, @mw10013! Merry Christmas to you! |
Closes #35 [ Refactor ] Decomplect handleTOTP() API
This refactor addresses the application API for totp data storage by
Totp data is ephemeral since it expires after some time. Session data is also ephemeral and this PR mimics Remix's API for session storage (https://remix.run/docs/en/main/utils/sessions#createsessionstorage).
Conveying expiresAt during create and update simplifies implementation using key-value stores where expiresAt may be metadata.
@dev-xo: Please let me know if you want to move forward with this PR and I'll incorporate any feedback, update README, and add migration notes since breaking change. It would also be helpful to have a dev version in npm for further testing with totp-starter-example and remix-auth-totp-cloudflare-example. And I'm open to learning how to deploy dev versions.