format project

This commit is contained in:
counterweight 2025-02-14 11:13:18 +01:00
parent 90d8e39eb3
commit c02cf8c12e
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
39 changed files with 2062 additions and 909 deletions

19
.eslintrc.json Normal file
View file

@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"rules": {
"prettier/prettier": [
"error",
{
"singleQuote": true,
"semi": false,
"trailingComma": "es5"
}
],
"no-console": "warn"
}
}

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"plugins": [
"prettier-plugin-ejs"
],
"singleQuote": true,
"semi": true,
"trailingComma": "es5"
}

9
eslint.config.mjs Normal file
View file

@ -0,0 +1,9 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ['**/*.js'], languageOptions: { sourceType: 'commonjs' } },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
pluginJs.configs.recommended,
]

1041
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,9 +20,20 @@
"start:containers": "docker compose up -d --build",
"stop:containers": "docker compose down",
"cli": "node src/cli.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint . --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,html,ejs}\""
},
"keywords": [],
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"@eslint/js": "^9.20.0",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^15.15.0",
"prettier": "^3.5.1",
"prettier-plugin-ejs": "^1.0.3"
}
}

View file

@ -3,9 +3,7 @@ const program = new Command();
const createAppInviteCommand = require('./commands/createAppInvite');
program
.version('1.0.0')
.description('CLI for managing web app tasks');
program.version('1.0.0').description('CLI for managing web app tasks');
program
.command('createAppInvite <inviterNpub>')

View file

@ -3,5 +3,5 @@ const DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS = 60 * 60 * 24 * 30;
module.exports = {
DEFAULT_SESSION_DURATION_SECONDS,
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS
}
DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS,
};

View file

@ -14,13 +14,16 @@ const sequelize = new Sequelize({
timestamps: false,
freezeTableName: true,
underscored: true,
quoteIdentifiers: false
quoteIdentifiers: false,
},
});
sequelize.sync().then(() => {
sequelize
.sync()
.then(() => {
console.log('Database synced');
}).catch(err => {
})
.catch((err) => {
console.error('Error syncing the database:', err);
});

View file

@ -1,21 +1,21 @@
class AlreadyUsedError extends Error {
constructor(message) {
super(message);
this.name = "AlreadyUsedError"
this.name = 'AlreadyUsedError';
}
}
class InvalidSignatureError extends Error {
constructor(message) {
super(message);
this.name = "InvalidSignatureError";
this.name = 'InvalidSignatureError';
}
}
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "AppInvitedUsedError";
this.name = 'AppInvitedUsedError';
}
}
@ -30,5 +30,5 @@ module.exports = {
AlreadyUsedError,
InvalidSignatureError,
NotFoundError,
ExpiredError
ExpiredError,
};

View file

@ -1,10 +1,9 @@
const sessionService = require('../services/sessionService');
async function attachPublicKeyMiddleware(req, res, next) {
const publicKey = await sessionService.getPublicKeyRelatedToSession(
req.cookies.sessionUuid
)
);
req.cookies.publicKey = publicKey;
next();

View file

@ -4,8 +4,8 @@ async function rejectIfNotAuthorizedMiddleware(req, res, next) {
if (!(await sessionService.isSessionAuthorized(req.cookies.sessionUuid))) {
return res.status(403).json({
success: false,
message: 'Your session is not authorized.'
})
message: 'Your session is not authorized.',
});
}
next();
}

View file

@ -1,22 +1,23 @@
const uuid = require("uuid");
const uuid = require('uuid');
const sessionService = require('../services/sessionService');
const constants = require('../constants');
async function setAndPersistNewSession(res) {
const sessionUuid = uuid.v7();
res.cookie('sessionUuid', sessionUuid, { httpOnly: true, maxAge: constants.DEFAULT_SESSION_DURATION_SECONDS * 1000 });
res.cookie('sessionUuid', sessionUuid, {
httpOnly: true,
maxAge: constants.DEFAULT_SESSION_DURATION_SECONDS * 1000,
});
return await sessionService.createSession(sessionUuid);
}
async function createSessionMiddleware(req, res, next) {
const sessionUuid = req.cookies.sessionUuid;
if (!sessionUuid) {
const newSession = await setAndPersistNewSession(res);
req.cookies.sessionUuid = newSession.uuid;
}
if (sessionUuid) {

View file

@ -1,12 +1,14 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const AppInviteCreated = sequelize.define('AppInviteCreated', {
const AppInviteCreated = sequelize.define(
'AppInviteCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
inviter_pub_key: {
type: DataTypes.STRING,
@ -14,10 +16,12 @@ const AppInviteCreated = sequelize.define('AppInviteCreated', {
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'app_invite_created',
}
}, {
tableName: 'app_invite_created'
});
);
module.exports = AppInviteCreated;

View file

@ -1,27 +1,31 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const ContactDetailsSet = sequelize.define('ContactDetailsSet', {
const ContactDetailsSet = sequelize.define(
'ContactDetailsSet',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
public_key: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
encrypted_contact_details: {
type: DataTypes.TEXT,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'contact_details_set',
}
}, {
tableName: 'contact_details_set'
});
);
module.exports = ContactDetailsSet;

View file

@ -1,31 +1,35 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const NostrChallengeCompleted = sequelize.define('NostrChallengeCompleted', {
const NostrChallengeCompleted = sequelize.define(
'NostrChallengeCompleted',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
challenge: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
signed_event: {
type: DataTypes.JSONB,
allowNull: false
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'nostr_challenge_completed',
}
}, {
tableName: 'nostr_challenge_completed'
});
);
module.exports = NostrChallengeCompleted;

View file

@ -1,27 +1,31 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const NostrChallengeCreated = sequelize.define('NostrChallengeCreated', {
const NostrChallengeCreated = sequelize.define(
'NostrChallengeCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
challenge: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
expires_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'nostr_challenge_created',
}
}, {
tableName: 'nostr_challenge_created'
});
);
module.exports = NostrChallengeCreated;

View file

@ -1,27 +1,31 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const NymSet = sequelize.define('NymSet', {
const NymSet = sequelize.define(
'NymSet',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
public_key: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
nym: {
type: DataTypes.TEXT,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'nym_set',
}
}, {
tableName: 'nym_set'
});
);
module.exports = NymSet;

View file

@ -1,23 +1,27 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const SessionCreated = sequelize.define('SessionCreated', {
const SessionCreated = sequelize.define(
'SessionCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
expires_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'session_created',
}
}, {
tableName: 'session_created'
});
);
module.exports = SessionCreated;

View file

@ -1,12 +1,14 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const SessionRelatedToPublickey = sequelize.define('SessionRelatedToPublickey', {
const SessionRelatedToPublickey = sequelize.define(
'SessionRelatedToPublickey',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
session_uuid: {
type: DataTypes.UUID,
@ -14,14 +16,16 @@ const SessionRelatedToPublickey = sequelize.define('SessionRelatedToPublickey',
},
public_key: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'session_related_to_public_key',
}
}, {
tableName: 'session_related_to_public_key'
});
);
module.exports = SessionRelatedToPublickey;

View file

@ -1,12 +1,14 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const SignUpChallengeCompleted = sequelize.define('SignUpChallengeCompleted', {
const SignUpChallengeCompleted = sequelize.define(
'SignUpChallengeCompleted',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
nostr_challenge_completed_uuid: {
type: DataTypes.UUID,
@ -14,18 +16,20 @@ const SignUpChallengeCompleted = sequelize.define('SignUpChallengeCompleted', {
},
app_invite_uuid: {
type: DataTypes.UUID,
allowNull: false
allowNull: false,
},
public_key: {
type: DataTypes.STRING,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'sign_up_challenge_completed',
}
}, {
tableName: 'sign_up_challenge_completed'
});
);
module.exports = SignUpChallengeCompleted;

View file

@ -1,27 +1,31 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../database');
const SignUpChallengeCreated = sequelize.define('SignUpChallengeCreated', {
const SignUpChallengeCreated = sequelize.define(
'SignUpChallengeCreated',
{
uuid: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
primaryKey: true
primaryKey: true,
},
nostr_challenge_uuid: {
type: DataTypes.UUID,
allowNull: false
allowNull: false,
},
app_invite_uuid: {
type: DataTypes.UUID,
allowNull: false
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false
allowNull: false,
},
},
{
tableName: 'sign_up_challenge_created',
}
}, {
tableName: 'sign_up_challenge_created'
});
);
module.exports = SignUpChallengeCreated;

View file

@ -9,7 +9,9 @@ class ContactDetails {
}
removeDetails(type, value) {
this.details = this.details.filter(detail => detail.type !== type || detail.value !== value);
this.details = this.details.filter(
(detail) => detail.type !== type || detail.value !== value
);
}
syncUi() {
@ -19,20 +21,20 @@ class ContactDetails {
const addedDetailFragment = this.buildContactDetailBadge(detail);
this.rootUiElement.appendChild(addedDetailFragment);
});
})
});
}
buildContactDetailBadge(detail) {
const fragment = document.createDocumentFragment();
const div = document.createElement("div");
div.className = "added-contact-detail badge";
const div = document.createElement('div');
div.className = 'added-contact-detail badge';
const p = document.createElement("p");
const p = document.createElement('p');
p.textContent = `${detail.type}: ${detail.value}`;
const button = document.createElement("button");
button.textContent = "Eliminar";
const button = document.createElement('button');
button.textContent = 'Eliminar';
button.onclick = () => {
this.removeDetails(detail.type, detail.value);
this.syncUi();
@ -48,7 +50,10 @@ class ContactDetails {
async getEncryptedContactDetails() {
const jsonString = JSON.stringify(this.details);
const encryptedContactDetails = await window.nostr.nip04.encrypt(await window.nostr.getPublicKey(), jsonString);
const encryptedContactDetails = await window.nostr.nip04.encrypt(
await window.nostr.getPublicKey(),
jsonString
);
return encryptedContactDetails;
}
}
@ -56,9 +61,11 @@ class ContactDetails {
let contactDetails;
window.onload = () => {
contactDetails = new ContactDetails(document.querySelector('#created-contact-details-list'));
contactDetails = new ContactDetails(
document.querySelector('#created-contact-details-list')
);
document.querySelectorAll('.contact-detail-add-button').forEach(button => {
document.querySelectorAll('.contact-detail-add-button').forEach((button) => {
button.addEventListener('click', function () {
const badge = this.parentElement;
const type = badge.getAttribute('data-type');
@ -76,29 +83,24 @@ window.onload = () => {
document
.querySelector('#submit-details-button')
.addEventListener(
'click',
async () => {
const encryptedContactDetails = await contactDetails.getEncryptedContactDetails();
await fetch('/api/set-contact-details',
{
.addEventListener('click', async () => {
const encryptedContactDetails =
await contactDetails.getEncryptedContactDetails();
await fetch('/api/set-contact-details', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify({ encryptedContactDetails })
body: JSON.stringify({ encryptedContactDetails }),
});
const nym = document.querySelector('#nym-input').value;
await fetch('/api/set-nym',
{
await fetch('/api/set-nym', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify({ nym })
body: JSON.stringify({ nym }),
});
});
}
);
};

View file

@ -1,22 +1,21 @@
window.onload = function () {
if (!window.nostr) {
console.log("Nostr extension not present");
console.log('Nostr extension not present');
document.querySelector('#nostr-signup').disabled = true;
document.querySelector('#no-extension-nudges').style.display = 'block';
} else {
console.log("Nostr extension present");
}
console.log('Nostr extension present');
}
};
async function acceptInvite() {
let challengeResponse;
try {
challengeResponse = await fetch('/api/signup/nostr-challenge', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
'Content-Type': 'application/json',
},
});
} catch (error) {
console.log(`Something went wrong: ${error}`);
@ -35,9 +34,9 @@ async function acceptInvite() {
const event = {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
tags: [["challenge", challenge]],
content: "Sign this challenge to authenticate",
pubkey: pubkey
tags: [['challenge', challenge]],
content: 'Sign this challenge to authenticate',
pubkey: pubkey,
};
let signedEvent;
@ -50,9 +49,9 @@ async function acceptInvite() {
let verifyResponse;
try {
verifyResponse = await fetch("/api/signup/nostr-verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
verifyResponse = await fetch('/api/signup/nostr-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedEvent),
});
} catch (error) {
@ -63,7 +62,7 @@ async function acceptInvite() {
if (verifyResponse.ok) {
document.querySelector('#sign-up-success').style.display = 'block';
setTimeout(() => {
window.location.href = "/createProfile";
window.location.href = '/createProfile';
}, 1000);
}
}

View file

@ -15,84 +15,82 @@ router.get('/signup/nostr-challenge', async (req, res) => {
let signUpChallenge;
try {
signUpChallenge = await invitesService.createSignUpChallenge(
inviteUuid
)
signUpChallenge = await invitesService.createSignUpChallenge(inviteUuid);
} catch (error) {
if (error instanceof errors.NotFoundError) {
return res.status(404).json({
success: false,
message: 'Could not find invite with that id.'
})
message: 'Could not find invite with that id.',
});
}
if (error instanceof errors.AlreadyUsedError) {
return res.status(410).json({
success: false,
message: 'That invite has already been used.'
})
message: 'That invite has already been used.',
});
}
return res.status(500).json({
success: false,
message: 'Unexpected error.'
})
message: 'Unexpected error.',
});
}
let relatedNostrChallenge;
try {
relatedNostrChallenge = await nostrService.getNostrChallenge(
signUpChallenge.nostr_challenge_uuid
)
);
} catch (error) {
return res.status(500).json({
success: false,
message: 'Unexpected error.'
})
message: 'Unexpected error.',
});
}
return res.status(200).json({ 'challenge': relatedNostrChallenge.challenge });
return res.status(200).json({ challenge: relatedNostrChallenge.challenge });
});
router.post("/signup/nostr-verify", async (req, res) => {
router.post('/signup/nostr-verify', async (req, res) => {
const signedEvent = req.body;
const sessionUuid = req.cookies.sessionUuid;
let completedSignUpChallenge;
try {
completedSignUpChallenge = await invitesService.verifySignUpChallenge(signedEvent);
completedSignUpChallenge =
await invitesService.verifySignUpChallenge(signedEvent);
} catch (error) {
if (error instanceof errors.ExpiredError) {
return res.status(410).json({
success: false,
message: 'The challenge has expired, request a new one.'
})
message: 'The challenge has expired, request a new one.',
});
}
if (error instanceof errors.AlreadyUsedError) {
return res.status(410).json({
success: false,
message: 'The challenge has been used, request a new one.'
})
message: 'The challenge has been used, request a new one.',
});
}
if (error instanceof errors.InvalidSignatureError) {
return res.status(400).json({
success: false,
message: 'The challenge signature is not valid.'
})
message: 'The challenge signature is not valid.',
});
}
}
await sessionService.relateSessionToPublicKey(
sessionUuid,
completedSignUpChallenge.public_key
)
);
return res.status(200).json({ success: true });
});
router.post(
"/set-contact-details",
'/set-contact-details',
rejectIfNotAuthorizedMiddleware,
attachPublicKeyMiddleware,
async (req, res) => {
@ -102,24 +100,21 @@ router.post(
if (!encryptedContactDetails) {
return res.status(400).json({
success: false,
message: 'Missing contact details.'
})
message: 'Missing contact details.',
});
}
await profileService.setContactDetails(
publicKey,
encryptedContactDetails
)
await profileService.setContactDetails(publicKey, encryptedContactDetails);
return res.status(200).json({
success: true,
message: 'Contact details set successfully.'
})
message: 'Contact details set successfully.',
});
}
);
router.post(
"/set-nym",
'/set-nym',
rejectIfNotAuthorizedMiddleware,
attachPublicKeyMiddleware,
async (req, res) => {
@ -129,19 +124,16 @@ router.post(
if (!nym) {
return res.status(400).json({
success: false,
message: 'Missing nym'
})
message: 'Missing nym',
});
}
await profileService.setNym(
publicKey,
nym
)
await profileService.setNym(publicKey, nym);
return res.status(200).json({
success: true,
message: 'Nym set successfully.'
})
message: 'Nym set successfully.',
});
}
);

View file

@ -2,7 +2,7 @@ const express = require('express');
const router = express.Router();
const redirectIfNotAuthorizedMiddleware = require('../middlewares/redirectIfNotAuthorizedMiddleware');
const invitesService = require('../services/invitesService')
const invitesService = require('../services/invitesService');
router.get('/', (req, res) => {
res.render('index', { uuid: req.cookies.sessionUuid });
@ -21,9 +21,8 @@ router.get('/invite/:inviteUuid', async (req, res) => {
}
if (await invitesService.isAppInviteSpent(inviteUuid)) {
return res.status(410).render('invite_spent', { invite })
return res.status(410).render('invite_spent', { invite });
}
} catch (error) {
console.error('Error fetching invite:', error);
return res.status(500).render('error', { message: 'An error occurred' });
@ -45,9 +44,8 @@ router.get('/invite/:inviteUuid', async (req, res) => {
}
if (await invitesService.isAppInviteSpent(inviteUuid)) {
return res.status(410).render('invite_spent', { invite })
return res.status(410).render('invite_spent', { invite });
}
} catch (error) {
console.error('Error fetching invite:', error);
return res.status(500).render('error', { message: 'An error occurred' });
@ -56,9 +54,13 @@ router.get('/invite/:inviteUuid', async (req, res) => {
return res.render('invite', { invite });
});
router.get('/createProfile', redirectIfNotAuthorizedMiddleware, async (req, res) => {
router.get(
'/createProfile',
redirectIfNotAuthorizedMiddleware,
async (req, res) => {
return res.status(200).render('createProfile');
})
}
);
router.get('/private', redirectIfNotAuthorizedMiddleware, (req, res) => {
res.render('private', {});

View file

@ -8,7 +8,9 @@ const SignUpChallengeCompleted = require('../models/SignUpChallengeCompleted');
const errors = require('../errors');
async function appInviteExists(inviteUuid) {
const invite = await AppInviteCreated.findOne({ where: { uuid: inviteUuid } });
const invite = await AppInviteCreated.findOne({
where: { uuid: inviteUuid },
});
if (invite) {
return true;
}
@ -16,16 +18,18 @@ async function appInviteExists(inviteUuid) {
}
async function getAppInvite(inviteUuid) {
const invite = await AppInviteCreated.findOne({ where: { uuid: inviteUuid } });
const invite = await AppInviteCreated.findOne({
where: { uuid: inviteUuid },
});
return invite;
}
async function isAppInviteSpent(appInviteUuid) {
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne({
where: {
app_invite_uuid: appInviteUuid
}
})
app_invite_uuid: appInviteUuid,
},
});
if (signUpChallengeCompleted) {
return true;
@ -37,82 +41,71 @@ async function createAppInvite(inviterPubKey) {
return await AppInviteCreated.create({
uuid: uuid.v7(),
inviter_pub_key: inviterPubKey,
created_at: new Date().toISOString()
}
);
created_at: new Date().toISOString(),
});
}
async function createSignUpChallenge(appInviteUuid) {
if (!(await appInviteExists(appInviteUuid))) {
throw new errors.NotFoundError("Invite doesn't exist.")
throw new errors.NotFoundError("Invite doesn't exist.");
}
if (await isAppInviteSpent(appInviteUuid)) {
throw new errors.AlreadyUsedError("Invite has already been used.")
throw new errors.AlreadyUsedError('Invite has already been used.');
}
const nostrChallenge = await nostrService.createNostrChallenge()
const nostrChallenge = await nostrService.createNostrChallenge();
return await SignUpChallengeCreated.create({
'uuid': uuid.v7(),
uuid: uuid.v7(),
nostr_challenge_uuid: nostrChallenge.uuid,
app_invite_uuid: appInviteUuid,
created_at: new Date().toISOString()
created_at: new Date().toISOString(),
});
}
)
}
async function verifySignUpChallenge(signedEvent) {
const challengeTag = signedEvent.tags.find(tag => tag[0] === "challenge");
const challengeTag = signedEvent.tags.find((tag) => tag[0] === 'challenge');
const challenge = challengeTag[1];
const nostrChallenge = await nostrService.getNostrChallenge(
null, challenge
);
const nostrChallenge = await nostrService.getNostrChallenge(null, challenge);
const signUpChallenge = await SignUpChallengeCreated.findOne({
where: {
nostr_challenge_uuid: nostrChallenge.uuid
}
})
nostr_challenge_uuid: nostrChallenge.uuid,
},
});
if (await nostrService.hasNostrChallengeBeenCompleted(challenge)) {
throw new errors.AlreadyUsedError("This challenge has already been used.");
throw new errors.AlreadyUsedError('This challenge has already been used.');
}
const completedNostrChallenge = await nostrService.verifyNostrChallenge(signedEvent);
const completedNostrChallenge =
await nostrService.verifyNostrChallenge(signedEvent);
const completedSignUpChallenge = await SignUpChallengeCompleted.create(
{
'uuid': uuid.v7(),
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()
}
);
created_at: new Date().toISOString(),
});
return completedSignUpChallenge;
}
async function isPublicKeySignedUp(publicKey) {
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne(
{
const signUpChallengeCompleted = await SignUpChallengeCompleted.findOne({
where: {
public_key: publicKey
}
}
)
public_key: publicKey,
},
});
if (signUpChallengeCompleted) {
return true;
}
return false;
}
module.exports = {
@ -122,5 +115,5 @@ module.exports = {
createAppInvite,
createSignUpChallenge,
verifySignUpChallenge,
isPublicKeySignedUp
isPublicKeySignedUp,
};

View file

@ -1,77 +1,78 @@
const uuid = require("uuid");
const crypto = require("crypto");
const uuid = require('uuid');
const crypto = require('crypto');
const { Op, TimeoutError } = require('sequelize');
const { verifyEvent } = require("nostr-tools");
const { verifyEvent } = require('nostr-tools');
const NostrChallengeCreated = require('../models/NostrChallengeCreated');
const NostrChallengeCompleted = require("../models/NostrChallengeCompleted");
const NostrChallengeCompleted = require('../models/NostrChallengeCompleted');
const constants = require('../constants');
const errors = require('../errors');
async function createNostrChallenge() {
const currentTimestamp = new Date();
const expiryTimestamp = new Date(currentTimestamp.getTime());
expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + constants.DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS);
expiryTimestamp.setSeconds(
expiryTimestamp.getSeconds() +
constants.DEFAULT_NOSTR_CHALLENGE_DURATION_SECONDS
);
const nostrChallenge = await NostrChallengeCreated.create({
'uuid': uuid.v7(),
challenge: crypto.randomBytes(32).toString("hex"),
uuid: uuid.v7(),
challenge: crypto.randomBytes(32).toString('hex'),
expires_at: expiryTimestamp.toISOString(),
created_at: currentTimestamp.toISOString()
created_at: currentTimestamp.toISOString(),
});
return nostrChallenge;
}
async function getNostrChallenge(nostrChallengeUuid = null, challenge = null) {
if (nostrChallengeUuid) {
return await NostrChallengeCreated.findOne({
where: {
'uuid': nostrChallengeUuid
}
})
uuid: nostrChallengeUuid,
},
});
}
if (challenge) {
return await NostrChallengeCreated.findOne({
where: {
challenge
}
})
challenge,
},
});
}
throw Error('You need to pass a uuid or a 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 challengeTag = signedEvent.tags.find((tag) => tag[0] === 'challenge');
const challenge = challengeTag[1];
if (!(await isNostrChallengeFresh(challenge))) {
throw TimeoutError("Challenge expired, request new one.");
throw TimeoutError('Challenge expired, request new one.');
}
if (await hasNostrChallengeBeenCompleted(challenge)) {
throw new errors.AlreadyUsedError("Challenge already used, request new one.");
throw new errors.AlreadyUsedError(
'Challenge already used, request new one.'
);
}
const isSignatureValid = verifyEvent(signedEvent);
if (!isSignatureValid) {
throw new errors.InvalidSignatureError("Signature is not valid.");
throw new errors.InvalidSignatureError('Signature is not valid.');
}
return await NostrChallengeCompleted.create({
'uuid': uuid.v7(),
uuid: uuid.v7(),
challenge: challenge,
signed_event: signedEvent,
public_key: signedEvent.pubkey,
created_at: new Date().toISOString()
}
);
created_at: new Date().toISOString(),
});
}
async function isNostrChallengeFresh(challengeString) {
@ -79,9 +80,9 @@ async function isNostrChallengeFresh(challengeString) {
where: {
challenge: challengeString,
expires_at: {
[Op.gt]: new Date()
}
}
[Op.gt]: new Date(),
},
},
});
if (nostrChallenge) {
@ -91,13 +92,11 @@ async function isNostrChallengeFresh(challengeString) {
}
async function hasNostrChallengeBeenCompleted(challengeString) {
const completedNostrChallenge = await NostrChallengeCompleted.findOne(
{
const completedNostrChallenge = await NostrChallengeCompleted.findOne({
where: {
challenge: challengeString
}
}
);
challenge: challengeString,
},
});
if (completedNostrChallenge) {
return true;
@ -105,11 +104,10 @@ async function hasNostrChallengeBeenCompleted(challengeString) {
return false;
}
module.exports = {
createNostrChallenge,
getNostrChallenge,
verifyNostrChallenge,
isNostrChallengeFresh,
hasNostrChallengeBeenCompleted
hasNostrChallengeBeenCompleted,
};

View file

@ -3,28 +3,24 @@ const ContactDetailsSet = require('../models/ContactDetailsSet');
const NymSet = require('../models/NymSet');
async function setContactDetails(publicKey, encryptedContactDetails) {
return await ContactDetailsSet.create(
{
'uuid': uuid.v7(),
return await ContactDetailsSet.create({
uuid: uuid.v7(),
public_key: publicKey,
encrypted_contact_details: encryptedContactDetails,
created_at: new Date().toISOString()
}
)
created_at: new Date().toISOString(),
});
}
async function setNym(publicKey, nym) {
return await NymSet.create(
{
'uuid': uuid.v7(),
return await NymSet.create({
uuid: uuid.v7(),
public_key: publicKey,
nym: nym,
created_at: new Date().toISOString()
}
)
created_at: new Date().toISOString(),
});
}
module.exports = {
setContactDetails,
setNym
setNym,
};

View file

@ -9,20 +9,22 @@ const constants = require('../constants');
async function createSession(sessionUuid) {
const currentTimestamp = new Date();
const expiryTimestamp = new Date(currentTimestamp.getTime());
expiryTimestamp.setSeconds(expiryTimestamp.getSeconds() + constants.DEFAULT_SESSION_DURATION_SECONDS);
expiryTimestamp.setSeconds(
expiryTimestamp.getSeconds() + constants.DEFAULT_SESSION_DURATION_SECONDS
);
return await SessionCreated.create({
uuid: sessionUuid,
created_at: currentTimestamp.toISOString(),
expires_at: expiryTimestamp.toISOString()
expires_at: expiryTimestamp.toISOString(),
});
}
async function isSessionValid(sessionUuid) {
const currentSession = await SessionCreated.findOne({
where: {
'uuid': sessionUuid
}
uuid: sessionUuid,
},
});
if (!currentSession) {
@ -38,30 +40,27 @@ async function isSessionValid(sessionUuid) {
async function relateSessionToPublicKey(sessionUuid, publicKey) {
if (!(await isSessionValid(sessionUuid))) {
throw Error("Session is not valid anymore.");
throw Error('Session is not valid anymore.');
}
if (!(await invitesService.isPublicKeySignedUp(publicKey))) {
throw Error("Public key is not signed up.");
throw Error('Public key is not signed up.');
}
return SessionRelatedToPublickey.create({
'uuid': uuid.v7(),
uuid: uuid.v7(),
session_uuid: sessionUuid,
public_key: publicKey,
created_at: new Date().toISOString()
created_at: new Date().toISOString(),
});
}
async function isSessionAuthorized(sessionUuid) {
const isSessionRelatedToPublicKey = await SessionRelatedToPublickey.findOne(
{
const isSessionRelatedToPublicKey = await SessionRelatedToPublickey.findOne({
where: {
session_uuid: sessionUuid
}
}
);
session_uuid: sessionUuid,
},
});
if (isSessionRelatedToPublicKey) {
return true;
@ -71,16 +70,13 @@ async function isSessionAuthorized(sessionUuid) {
}
async function getPublicKeyRelatedToSession(sessionUuid) {
const sessionRelatedToPublickey = await SessionRelatedToPublickey.findOne(
{
const sessionRelatedToPublickey = await SessionRelatedToPublickey.findOne({
where: {
session_uuid: sessionUuid
}
}
);
session_uuid: sessionUuid,
},
});
return sessionRelatedToPublickey.public_key;
}
module.exports = {
@ -88,5 +84,5 @@ module.exports = {
isSessionValid,
relateSessionToPublicKey,
isSessionAuthorized,
getPublicKeyRelatedToSession
}
getPublicKeyRelatedToSession,
};

View file

@ -1,11 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Crear perfil</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/seca.css">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/css/seca.css" />
<script src="/javascript/createProfile.js"></script>
</head>
@ -13,41 +12,66 @@
<h1>Crea tu perfil</h1>
<p>Tu clave de Nostr ya es parte de la seca.</p>
<p>Añade detalles a tu perfil para poder empezar a comerciar.</p>
</p>
<hr>
<hr />
<form onsubmit="return false">
<label>Pseudónimo (Nym):<input type="text" name="nym" id="nym-input"></label>
<label
>Pseudónimo (Nym):<input type="text" name="nym" id="nym-input"
/></label>
<div id="contacts">
<p>Añade métodos de contacto para poder hablar con otros miembros.</p>
<div class="badges">
<div class="badge" data-type="whatsapp">📱 Teléfono <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="whatsapp">📱 WhatsApp <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="telegram">📩 Telegram <input><button
class="contact-detail-add-button"></button>>Añadir</button></div>
<div class="badge" data-type="email">📧 Email <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="nostr">📧 Nostr <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="signal">📧 Signal <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="matrix">📧 Matrix <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="xmpp">📧 XMPP <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="simplex">📧 Simplex <input><button
class="contact-detail-add-button">Añadir</button></div>
<div class="badge" data-type="whatsapp">
📱 Teléfono <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="whatsapp">
📱 WhatsApp <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="telegram">
📩 Telegram <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="email">
📧 Email <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="nostr">
📧 Nostr <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="signal">
📧 Signal <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="matrix">
📧 Matrix <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="xmpp">
📧 XMPP <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
<div class="badge" data-type="simplex">
📧 Simplex <input /><button class="contact-detail-add-button">
Añadir
</button>
</div>
</div>
</div>
<div id="created-contact-details-section">
<p>Contactos añadidos</p>
<div id="created-contact-details-list">
</div>
<div id="created-contact-details-list"></div>
</div>
<button id="submit-details-button" type="submit">Crear tu perfil</button>
</form>
</body>
</html>

View file

@ -1,17 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Error</title>
</head>
<body>
<h1>Error</h1>
<p>
<%= message %>
</p>
<p><%= message %></p>
</body>
</html>

View file

@ -1,10 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello World</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/javascript/index.js"></script>
</head>
@ -15,9 +14,13 @@
<button type="submit">Login con extensión de Nostr</button>
</form>
<p>
¿No tienes cuenta de Nostr? <a href="https://start.njump.me/" target="_blank" rel="noopener noreferrer">Crea una
gratis</a>.
¿No tienes cuenta de Nostr?
<a
href="https://start.njump.me/"
target="_blank"
rel="noopener noreferrer"
>Crea una gratis</a
>.
</p>
</body>
</html>

View file

@ -1,43 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Invite Details</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/javascript/invite.js"></script>
</head>
<body>
<h1>¡Has sido invitado!</h1>
<p>Has sido invitado a la seca.</p>
<p>Invite UUID: <%= invite.uuid %>
</p>
<p>Invite UUID: <%= invite.uuid %></p>
<p>Usa tu extensión de Nostr para darte de alta:</p>
<form onsubmit="acceptInvite();return false">
<button id="nostr-signup" type="submit">Alta con Nostr</button>
</form>
<div id="rejected-nostr-nudges" style="display: none">
<p>Ups, parece que no has aceptado que usemos tus claves. Si te has equivocado, puedes intentarlo de nuevo.</p>
<p>
Ups, parece que no has aceptado que usemos tus claves. Si te has
equivocado, puedes intentarlo de nuevo.
</p>
</div>
<div id="no-extension-nudges" style="display: none">
<p>¡Atención! No se ha encontrado una extensión de Nostr en tu navegador. Puedes usar: </p>
<p>
¡Atención! No se ha encontrado una extensión de Nostr en tu navegador.
Puedes usar:
</p>
<ul>
<li>Firefox
<li>
Firefox
<ul>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/alby/" target="_blank"
rel="noopener noreferrer">Alby</a></li>
<li><a href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/" target="_blank"
rel="noopener noreferrer">nos2x-fox</a></li>
<li>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/alby/"
target="_blank"
rel="noopener noreferrer"
>Alby</a
>
</li>
<li>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/nos2x-fox/"
target="_blank"
rel="noopener noreferrer"
>nos2x-fox</a
>
</li>
</ul>
</li>
<li>
Chrome
<ul>
<li><a href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
target="_blank" rel="noopener noreferrer">Alby</a></li>
<li><a href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
target="_blank" rel="noopener noreferrer">nos2x</a></li>
<li>
<a
href="https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1"
target="_blank"
rel="noopener noreferrer"
>Alby</a
>
</li>
<li>
<a
href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp"
target="_blank"
rel="noopener noreferrer"
>nos2x</a
>
</li>
</ul>
</li>
</ul>
@ -46,8 +75,14 @@
<p>¡Bien! Hemos dado de alta tu clave de Nostr.</p>
<p>Te vamos a redirigir a la seca, espera un momento.</p>
</div>
<p>¿No tienes cuenta de Nostr? <a href="https://start.njump.me/" target="_blank" rel="noopener noreferrer">Crea
una aquí.</a< /p>
<p>
¿No tienes cuenta de Nostr?
<a
href="https://start.njump.me/"
target="_blank"
rel="noopener noreferrer"
>Crea una aquí.</a
>
</p>
</body>
</html>

View file

@ -1,20 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Invite Details</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Invite Details</h1>
<p>Invite UUID: <%= invite.uuid %>
</p>
<p>Invite UUID: <%= invite.uuid %></p>
<h2>Wait right there buddy... that invite has been spent already!</h2>
<form>
<button onclick="" disabled>Click to accept invite with Nostr</button>
</form>
</body>
</html>

View file

@ -1,15 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Private page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Private page</h1>
<p>If you are here, it's because your npub is white listed, and your session cookie is related to it.</p>
<p>
If you are here, it's because your npub is white listed, and your session
cookie is related to it.
</p>
</body>
</html>