worksss
This commit is contained in:
parent
768efaf3a2
commit
fb9832fabb
8 changed files with 306 additions and 92 deletions
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
npub -> publicKey
|
||||
|
||||
exports.createInvitedNpub = createPublicKeyInvite;
|
||||
exports.isNpubInvited = isPublicKeyInvited;
|
||||
|
||||
invitedNpubService -> PublicKeyInvitedService
|
||||
34
src/errors.js
Normal file
34
src/errors.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
class ChallengedUsedError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ChallengeUsedError";
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidSignatureError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "InvalidSignatureError";
|
||||
}
|
||||
}
|
||||
|
||||
class AppInvitedUsedError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "AppInvitedUsedError";
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "AppInvitedUsedError";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ChallengedUsedError,
|
||||
InvalidSignatureError,
|
||||
AppInvitedUsedError,
|
||||
NotFoundError
|
||||
};
|
||||
31
src/models/SignUpChallengeCompleted.js
Normal file
31
src/models/SignUpChallengeCompleted.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../database');
|
||||
|
||||
const SignUpChallengeCompleted = sequelize.define('SignUpChallengeCompleted', {
|
||||
uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
primaryKey: true
|
||||
},
|
||||
nostr_challenge_completed_uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
app_invite_uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false
|
||||
},
|
||||
public_key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'sign_up_challenge_completed'
|
||||
});
|
||||
|
||||
module.exports = SignUpChallengeCompleted;
|
||||
27
src/models/SignUpChallengeCreated.js
Normal file
27
src/models/SignUpChallengeCreated.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../database');
|
||||
|
||||
const SignUpChallengeCreated = sequelize.define('SignUpChallengeCreated', {
|
||||
uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
primaryKey: true
|
||||
},
|
||||
nostr_challenge_uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false
|
||||
},
|
||||
app_invite_uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'sign_up_challenge_created'
|
||||
});
|
||||
|
||||
module.exports = SignUpChallengeCreated;
|
||||
|
|
@ -1,25 +1,40 @@
|
|||
async function acceptInvite() {
|
||||
const publicKey = await window.nostr.getPublicKey();
|
||||
|
||||
// check if there is nostr extension
|
||||
if (!window.nostr) {
|
||||
console.log("No Nostr extension found.");
|
||||
return { success: false, error: "No Nostr extension detected." };
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sign-public-key-up', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
publicKey
|
||||
})
|
||||
const challengeResponse = await fetch("/api/signup/nostr-challenge");
|
||||
if (!challengeResponse.ok) throw new Error("Failed to fetch challenge");
|
||||
const { challenge } = await challengeResponse.json();
|
||||
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
|
||||
const event = {
|
||||
kind: 22242,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["challenge", challenge]],
|
||||
content: "Sign this challenge to authenticate",
|
||||
pubkey: pubkey
|
||||
};
|
||||
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
|
||||
const verifyResponse = await fetch("/api/signup/nostr-verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(signedEvent),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('invited-npub record created successfully:', data);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Failed to create invited-npub record:', error);
|
||||
if (verifyResponse.status === 200) {
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('An error occurred:', error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (error) { }
|
||||
}
|
||||
|
|
@ -5,6 +5,9 @@ const crypto = require("crypto");
|
|||
const invitesService = require('../services/invitesService');
|
||||
const sessionService = require('../services/sessionService');
|
||||
const nostrService = require('../services/nostrService');
|
||||
const { error } = require('console');
|
||||
const { TimeoutError } = require('sequelize');
|
||||
const errors = require('../errors');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -58,54 +61,40 @@ router.post('/sign-public-key-up', async (req, res) => {
|
|||
});
|
||||
|
||||
|
||||
router.get('/nostr-challenge', async (req, res) => {
|
||||
const nostrChallenge = await nostrService.createNostrChallenge();
|
||||
res.json({ 'challenge': nostrChallenge.challenge });
|
||||
router.get('/signup/nostr-challenge', async (req, res) => {
|
||||
const inviteUuid = req.cookies.inviteUuid;
|
||||
|
||||
const signUpChallenge = await invitesService.createSignUpChallenge(
|
||||
inviteUuid
|
||||
)
|
||||
|
||||
const relatedNostrChallenge = await nostrService.getNostrChallenge(
|
||||
signUpChallenge.nostr_challenge_uuid
|
||||
)
|
||||
|
||||
res.status(200).json({ 'challenge': relatedNostrChallenge.challenge });
|
||||
});
|
||||
|
||||
|
||||
router.post("/nostr-verify", async (req, res) => {
|
||||
router.post("/signup/nostr-verify", async (req, res) => {
|
||||
const signedEvent = req.body;
|
||||
|
||||
if (!signedEvent || !signedEvent.tags) {
|
||||
return res.status(400).json({ success: false, error: "Invalid event format" });
|
||||
try {
|
||||
console.log(`Starting nostr-verify with event: ${signedEvent}`);
|
||||
const completedSignUpChallenge = await invitesService.verifySignUpChallenge(signedEvent);
|
||||
console.log(`Finished nostr-verify`);
|
||||
} catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
console.error('The challenge is outdated.');
|
||||
}
|
||||
if (error instanceof errors.ChallengedUsedError) {
|
||||
console.error('The challenge was already used, request a new one.');
|
||||
}
|
||||
if (error instanceof errors.InvalidSignatureError) {
|
||||
console.error('Signature is not valid.')
|
||||
}
|
||||
}
|
||||
|
||||
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
|
||||
if (!challengeTag) {
|
||||
return res.status(400).json({ success: false, error: "No challenge tag found" });
|
||||
}
|
||||
|
||||
const challenge = challengeTag[1];
|
||||
|
||||
if (!(await nostrService.isNostrChallengeFresh(challenge))) {
|
||||
return res.status(410).json({ success: false, error: "Challenge expired, request new one." })
|
||||
}
|
||||
|
||||
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
|
||||
return res.status(410).json({ success: false, error: "Challenge already used, request new one." })
|
||||
}
|
||||
|
||||
const isSignatureValid = verifyEvent(signedEvent);
|
||||
if (!isSignatureValid) {
|
||||
return res.status(400).json({ success: false, error: "Invalid signature" });
|
||||
}
|
||||
|
||||
if (!invitesService.isPublicKeyInvited(signedEvent.pubkey)) {
|
||||
return res.status(400).json(
|
||||
{
|
||||
success: false,
|
||||
error: "Valid signature, but npub is not invited to app."
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await nostrService.completeNostrChallenge(
|
||||
challenge,
|
||||
signedEvent
|
||||
)
|
||||
|
||||
return res.json({ success: true, signedEvent });
|
||||
return res.status(200).json({ success: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
const uuid = require('uuid');
|
||||
|
||||
const nostrService = require('./nostrService');
|
||||
const AppInviteCreated = require('../models/AppInviteCreated');
|
||||
const PublicKeySignedUp = require('../models/PublicKeySignedUp');
|
||||
const SignUpChallengeCreated = require('../models/SignUpChallengeCreated');
|
||||
const SignUpChallengeCompleted = require('../models/SignUpChallengeCompleted');
|
||||
|
||||
|
||||
const errors = require('../errors');
|
||||
const NostrChallengeCompleted = require('../models/NostrChallengeCompleted');
|
||||
const { sortEvents } = require('nostr-tools');
|
||||
|
||||
async function appInviteExists(inviteUuid) {
|
||||
const invite = await AppInviteCreated.findOne({ where: { uuid: inviteUuid } });
|
||||
|
|
@ -16,14 +24,14 @@ async function getAppInvite(inviteUuid) {
|
|||
return invite;
|
||||
}
|
||||
|
||||
async function isAppInviteSpent(inviteUuid) {
|
||||
const invitedNpub = await PublicKeySignedUp.findOne({
|
||||
async function isAppInviteSpent(appInviteUuid) {
|
||||
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne({
|
||||
where: {
|
||||
app_invite_uuid: inviteUuid
|
||||
app_invite_uuid: appInviteUuid
|
||||
}
|
||||
})
|
||||
|
||||
if (invitedNpub) {
|
||||
if (signUpChallengeCompleted) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -38,6 +46,79 @@ async function createAppInvite(inviterNpub) {
|
|||
);
|
||||
}
|
||||
|
||||
async function createSignUpChallenge(appInviteUuid) {
|
||||
|
||||
if (!(await appInviteExists(appInviteUuid))) {
|
||||
throw new errors.NotFoundError("Invite doesn't exist.")
|
||||
}
|
||||
|
||||
if (await isAppInviteSpent(appInviteUuid)) {
|
||||
throw new errors.AppInvitedUsedError("Invite has already been used.")
|
||||
}
|
||||
|
||||
const nostrChallenge = await nostrService.createNostrChallenge()
|
||||
|
||||
return await SignUpChallengeCreated.create({
|
||||
'uuid': uuid.v7(),
|
||||
nostr_challenge_uuid: nostrChallenge.uuid,
|
||||
app_invite_uuid: appInviteUuid,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function hasSignUpChallengeBeenCompleted(nostrChallengeCompletedUuid) {
|
||||
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne(
|
||||
{
|
||||
where:
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function verifySignUpChallenge(signedEvent) {
|
||||
|
||||
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
|
||||
const challenge = challengeTag[1];
|
||||
|
||||
const nostrChallenge = await nostrService.getNostrChallenge(
|
||||
null, challenge
|
||||
);
|
||||
|
||||
console.log(`Found this nostr challenge: ${nostrChallenge}`);
|
||||
|
||||
const signUpChallenge = await SignUpChallengeCreated.findOne({
|
||||
where: {
|
||||
nostr_challenge_uuid: nostrChallenge.uuid
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Found this signup challenge: ${signUpChallenge}`);
|
||||
|
||||
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
|
||||
throw new errors.ChallengedUsedError("This challenge has already been used.");
|
||||
}
|
||||
console.log(`I'm gonna verify the nostr challenge`);
|
||||
const completedNostrChallenge = await nostrService.verifyNostrChallenge(signedEvent);
|
||||
console.log(`Verified the NostrChallenge: ${completedNostrChallenge}`);
|
||||
|
||||
const completedSignUpChallenge = await SignUpChallengeCompleted.create(
|
||||
{
|
||||
'uuid': uuid.v7(),
|
||||
nostr_challenge_completed_uuid: completedNostrChallenge.uuid,
|
||||
app_invite_uuid: signUpChallenge.app_invite_uuid,
|
||||
public_key: completedNostrChallenge.public_key,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
console.log(`Verified the SignUpChallenge: ${completedSignUpChallenge}`);
|
||||
|
||||
|
||||
return completedSignUpChallenge;
|
||||
}
|
||||
|
||||
async function signUpPublicKey(inviteUuid, publicKey) {
|
||||
|
||||
if (await isAppInviteSpent(inviteUuid)) {
|
||||
|
|
@ -68,6 +149,8 @@ module.exports = {
|
|||
getAppInvite,
|
||||
isAppInviteSpent,
|
||||
createAppInvite,
|
||||
createSignUpChallenge,
|
||||
verifySignUpChallenge,
|
||||
signUpPublicKey,
|
||||
isPublicKeySignedUp
|
||||
};
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
const uuid = require("uuid");
|
||||
const crypto = require("crypto");
|
||||
const { Op } = require('sequelize');
|
||||
const { Op, TimeoutError } = require('sequelize');
|
||||
const { verifyEvent } = require("nostr-tools");
|
||||
|
||||
const NostrChallengeCreated = require('../models/NostrChallengeCreated');
|
||||
const NostrChallengeCompleted = require("../models/NostrChallengeCompleted");
|
||||
|
||||
const constants = require('../constants');
|
||||
|
||||
const errors = require('../errors');
|
||||
|
||||
async function createNostrChallenge() {
|
||||
|
||||
|
|
@ -24,6 +25,59 @@ async function createNostrChallenge() {
|
|||
return nostrChallenge;
|
||||
}
|
||||
|
||||
async function getNostrChallenge(nostrChallengeUuid = null, challenge = null) {
|
||||
|
||||
if (nostrChallengeUuid) {
|
||||
return await NostrChallengeCreated.findOne({
|
||||
where: {
|
||||
'uuid': nostrChallengeUuid
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (challenge) {
|
||||
return await NostrChallengeCreated.findOne({
|
||||
where: {
|
||||
challenge
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
throw Error('You need to pass a uuid or a challenge.')
|
||||
|
||||
}
|
||||
|
||||
async function verifyNostrChallenge(signedEvent) {
|
||||
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
|
||||
const challenge = challengeTag[1];
|
||||
|
||||
console.log("Checking if fresh")
|
||||
if (!(await isNostrChallengeFresh(challenge))) {
|
||||
throw TimeoutError("Challenge expired, request new one.");
|
||||
}
|
||||
|
||||
console.log("Checking if completed")
|
||||
if (await hasNostrChallengeBeenCompleted(challenge)) {
|
||||
throw new errors.ChallengedUsedError("Challenge already used, request new one.");
|
||||
}
|
||||
|
||||
console.log("Checking if valid")
|
||||
const isSignatureValid = verifyEvent(signedEvent);
|
||||
if (!isSignatureValid) {
|
||||
throw new errors.InvalidSignatureError("Signature is not valid.");
|
||||
}
|
||||
|
||||
console.log("Persisting")
|
||||
return await NostrChallengeCompleted.create({
|
||||
'uuid': uuid.v7(),
|
||||
challenge: challenge,
|
||||
signed_event: signedEvent,
|
||||
public_key: signedEvent.pubkey,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function isNostrChallengeFresh(challengeString) {
|
||||
const nostrChallenge = await NostrChallengeCreated.findOne({
|
||||
where: {
|
||||
|
|
@ -55,23 +109,11 @@ async function hasNostrChallengeBeenCompleted(challengeString) {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function completeNostrChallenge(
|
||||
challenge,
|
||||
signedEvent
|
||||
) {
|
||||
await NostrChallengeCompleted.create({
|
||||
'uuid': uuid.v7(),
|
||||
challenge: challenge,
|
||||
signed_event: signedEvent,
|
||||
public_key: signedEvent.pubkey,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
exports.createNostrChallenge = createNostrChallenge;
|
||||
exports.isNostrChallengeFresh = isNostrChallengeFresh;
|
||||
exports.hasNostrChallengeBeenCompleted = hasNostrChallengeBeenCompleted;
|
||||
exports.completeNostrChallenge = completeNostrChallenge;
|
||||
module.exports = {
|
||||
createNostrChallenge,
|
||||
getNostrChallenge,
|
||||
verifyNostrChallenge,
|
||||
isNostrChallengeFresh,
|
||||
hasNostrChallengeBeenCompleted
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue