Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# pointers-specific
peers.json

# Logs
logs
*.log
Expand Down
23 changes: 23 additions & 0 deletions config/db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"development": {
"username": "root",
"password": "rootpassword",
"database": "pointers",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
10 changes: 10 additions & 0 deletions config/peers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"peers": [
{
"url": "https://dev.pointers.website"
},
{
"url": "https://pointers.website"
}
]
}
2 changes: 1 addition & 1 deletion lib/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import PointerKey from '../models/PointerKey.js';
dotenv.config();

const sequelize = new Sequelize({
database: 'pointers',
database: process.env.DB_DATABASE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: '127.0.0.1',
Expand Down
67 changes: 67 additions & 0 deletions lib/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Op } from 'sequelize';
import sequelize from './database.js';

const searchUsers = async (q) => {
if (!q) {
return [];
}
if (q.length < 3) {
return [];
}
const domain = process.env.DOMAIN;
const userNames = await sequelize.models.UserName.findAll({
where: {
name: {
[Op.substring]: q,
},
},
include: {
model: sequelize.models.User,
attributes: ['id', 'hash'],
},
raw: true,
nest: true,
});
const users = userNames.reduce((acc, curr) => {
if (!curr.User?.id) {
return acc;
}
if (!acc.find((user) => user.id === curr.User.id)) {
acc.push({
...curr.User,
names: [
{
name: curr.name,
main: Boolean(curr.main),
absolute: `${curr.name}@${domain}`,
},
],
});
} else {
acc.find((user) => user.id === curr.User.id).names.push({
name: curr.name,
main: curr.main,
absolute: `${curr.name}@${domain}`,
});
}
return acc;
}, []);
users.forEach((user) => {
user.names.sort((a, b) => {
if (a.main && !b.main) {
return -1;
} else if (!a.main && b.main) {
return 1;
} else {
return 0;
}
});
// Add the URL to the user's profile
user.url = `https://${process.env.DOMAIN}/u/${user.hash}`;
user.domain = domain;
});

return users;
};

export { searchUsers };
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"test": "yarn dev & yarn run cypress open"
},
"dependencies": {
"axios": "^1.2.3",
"bcrypt": "^5.1.0",
"cheerio": "^1.0.0-rc.12",
"connect-flash": "^0.1.1",
Expand Down
19 changes: 17 additions & 2 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ header#header {
}

header#header h1 {
font-size: 2rem;
font-weight: 700;
margin: 0;
color: rgba(255, 255, 255, 1);
}
Expand All @@ -59,6 +61,11 @@ header#header h1 a {
text-decoration: none;
}

header#header h1 .subheading {
font-size: 1.3rem;
font-weight: 400;
}

header#header #logo {
display: flex;
align-items: center;
Expand Down Expand Up @@ -186,6 +193,14 @@ footer#footer a {
margin: 0.5rem 0;
}

.item aside {
font-style: italic;
white-space: unset;
color: #666;
font-size: 0.9rem;
margin-top: 0.25rem;
}

label {
display: block;
margin-bottom: 0.5rem;
Expand Down Expand Up @@ -313,7 +328,7 @@ a.pointer:hover {
.pointer .pointer__title {
display: flex;
align-items: center;
gap: 0.25rem;
gap: 0.25rem;
}

.pointer .pointer__icon-and-name {
Expand Down Expand Up @@ -366,4 +381,4 @@ form .edit__icon-field .edit__icon {
form .edit__icon-field .edit__no-icon {
color: #777;
font-weight: 300;
}
}
80 changes: 41 additions & 39 deletions router.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import {
encryptRsa,
generateKeyPair,
} from './lib/encryption.js';
import axios from 'axios';
import { readFile } from 'fs/promises';
import { searchUsers } from './lib/search.js';

const api = express.Router();
const frontend = express.Router();
Expand Down Expand Up @@ -227,47 +230,29 @@ frontend.get('/search', redirectIfNotLoggedIn, async (req, res) => {
error: 'Search query must be at least 3 characters long.',
});
}
const userNames = await sequelize.models.UserName.findAll({
where: {
name: {
[Op.substring]: q,
},
},
include: {
model: sequelize.models.User,
attributes: ['id', 'emailAddress', 'bio', 'avatar', 'hash'],
},
raw: true,
nest: true,
});
const users = userNames.reduce((acc, curr) => {
if (!curr.User?.id) {
return acc;
}
if (!acc.find((user) => user.id === curr.User.id)) {
acc.push({
...curr.User,
names: [{ name: curr.name, main: curr.main }],
});
} else {
acc.find((user) => user.id === curr.User.id).names.push({
name: curr.name,
main: curr.main,
const users = await searchUsers(q);
// Add remote results from federated instances ('peers')
const peersConfig = JSON.parse(
await readFile(new URL('./config/peers.json', import.meta.url)),
);
for (const peer of peersConfig.peers) {
try {
const { data } = await axios.get(
`${peer.url}/api/users?q=${encodeURIComponent(q)}`,
);
// Add the 'remote' property to each user so we can render them differently
data.forEach((user) => {
user.remote = true;
});
}
return acc;
}, []);
users.forEach((user) => {
user.names.sort((a, b) => {
if (a.main && !b.main) {
return -1;
} else if (!a.main && b.main) {
return 1;
} else {
return 0;
users.push(...data);
} catch (err) {
// If it's a 404, it's because the peer is running an older version
// and doesn't have the search endpoint. We can ignore it.
if (err.response.status !== 404) {
console.error(err);
}
});
});
}
}
res.render('search', { users, q });
});

Expand Down Expand Up @@ -842,4 +827,21 @@ api.get(
},
);

// Public search API
api.get('/users', async (req, res) => {
const { q } = req.query;
if (!q) {
return res
.status(400)
.json({ error: 'Please provide a search query.' });
}
if (q.length < 3) {
return res.status(400).json({
error: 'Please provide a search query of at least 3 characters.',
});
}
const users = await searchUsers(q);
return res.json(users);
});

export { api, frontend, auth };
2 changes: 1 addition & 1 deletion views/layout.eta
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<header id="header">
<div id="logo">
<img src="/images/arrow.png" alt="logo" />
<h1><a href="/">Pointers</a></h1>
<h1><a href="/">Pointers</a> // <span class="subheading"><%= process.env.DOMAIN %></span></h1>
</div>
<nav id="main-nav">
<%~ includeFile('./partials/navigation.eta', it) %>
Expand Down
3 changes: 2 additions & 1 deletion views/search.eta
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
<% if (it.users.length > 0) { %>
<% it.users.forEach((user) => { %>
<div class="user item">
<a href="/u/<%= user.hash %>"><%= user.names[0].name %></a>
<a href="<%= user.url %>"><% if (user.remote) {%><%= user.names[0].absolute %><% } else { %><%= user.names[0].name %><% } %></a>
<% if (user.remote) {%><aside>This profile is hosted on another Pointers instance, <strong><%= user.host %></strong>. You will need to create an account on that instance to view it.</aside><%}%>
<% if (user.bio) { %><p class="description"><%= user.bio %></p><% } %>
</div>
<% }) %>
Expand Down
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,15 @@ at-least-node@^1.0.0:
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==

axios@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.3.tgz#31a3d824c0ebf754a004b585e5f04a5f87e6c4ff"
integrity sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"

babel-jest@^29.3.1:
version "29.3.1"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44"
Expand Down Expand Up @@ -1961,6 +1970,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==

follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==

form-data@^2.3.3:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
Expand Down Expand Up @@ -3561,7 +3575,7 @@ proxy-agent@^3.0.3:
proxy-from-env "^1.0.0"
socks-proxy-agent "^4.0.1"

proxy-from-env@^1.0.0:
proxy-from-env@^1.0.0, proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
Expand Down