mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-04 00:20:45 +00:00
feat: add share service and share-by-email to /share
This commit is contained in:
@@ -7,6 +7,8 @@ Comments beginning with `// track:`. See
|
||||
|
||||
- `track: type check`:
|
||||
A condition that's used to check the type of an imput.
|
||||
- `track: adapt`
|
||||
A value can by adapted from another type at this line.
|
||||
- `track: bounds check`:
|
||||
A condition that's used to check the bounds of an array
|
||||
or other list-like entity.
|
||||
@@ -22,3 +24,13 @@ Comments beginning with `// track:`. See
|
||||
A common pattern where a prefix string is "sliced off"
|
||||
of another string to obtain a significant value, such
|
||||
as an indentifier.
|
||||
- `track: actor type`
|
||||
The sub-type of an Actor object is checked.
|
||||
- `track: scoping iife`
|
||||
An immediately-invoked function expression specifically
|
||||
used to reduce scope clutter.
|
||||
- `track: good candidate for sequence`
|
||||
Some code involves a series of similar steps,
|
||||
or there's a common behavior that should happen
|
||||
in between. The Sequence class is good for this so
|
||||
it might be a worthy migration.
|
||||
|
||||
@@ -283,6 +283,9 @@ const install = async ({ services, app, useapi }) => {
|
||||
|
||||
const { ProtectedAppService } = require('./services/ProtectedAppService');
|
||||
services.registerService('__protected-app', ProtectedAppService);
|
||||
|
||||
const { ShareService } = require('./services/ShareService');
|
||||
services.registerService('share', ShareService);
|
||||
}
|
||||
|
||||
const install_legacy = async ({ services }) => {
|
||||
|
||||
@@ -41,7 +41,12 @@ module.exports = class APIError {
|
||||
status: 400,
|
||||
message: ({ message }) => `error: ${message}`,
|
||||
},
|
||||
|
||||
'disallowed_value': {
|
||||
status: 400,
|
||||
message: ({ key ,allowed }) =>
|
||||
`value of ${quot(key)} must be one of: ` +
|
||||
allowed.map(v => quot(v)).join(', ')
|
||||
},
|
||||
// Things
|
||||
'disallowed_thing': {
|
||||
status: 400,
|
||||
|
||||
@@ -23,6 +23,7 @@ const v0_2 = async (req, res) => {
|
||||
const svc_email = req.services.get('email');
|
||||
const svc_permission = req.services.get('permission');
|
||||
const svc_notification = req.services.get('notification');
|
||||
const svc_share = req.services.get('share');
|
||||
|
||||
const lib_typeTagged = req.services.get('lib-type-tagged');
|
||||
|
||||
@@ -148,6 +149,8 @@ const v0_2 = async (req, res) => {
|
||||
}
|
||||
recipients_work.lockin();
|
||||
|
||||
// track: good candidate for sequence
|
||||
|
||||
// Expect: each value should be a valid username or email
|
||||
for ( const item of recipients_work.list() ) {
|
||||
const { value, i } = item;
|
||||
@@ -155,9 +158,10 @@ const v0_2 = async (req, res) => {
|
||||
if ( typeof value !== 'string' ) {
|
||||
item.invalid = true;
|
||||
result.recipients[i] =
|
||||
APIError.create('invalid_username_of_email', null, {
|
||||
APIError.create('invalid_username_or_email', null, {
|
||||
value,
|
||||
})
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( value.match(config.username_regex) ) {
|
||||
@@ -165,7 +169,7 @@ const v0_2 = async (req, res) => {
|
||||
continue;
|
||||
}
|
||||
if ( validator.isEmail(value) ) {
|
||||
item.type = 'username';
|
||||
item.type = 'email';
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -182,12 +186,13 @@ const v0_2 = async (req, res) => {
|
||||
// Expect: no emails specified yet
|
||||
// AND usernames exist
|
||||
for ( const item of recipients_work.list() ) {
|
||||
if ( item.type === 'email' ) {
|
||||
const allowed_types = ['email', 'username'];
|
||||
if ( ! allowed_types.includes(item.type) ) {
|
||||
item.invalid = true;
|
||||
result.recipients[item.i] =
|
||||
APIError.create('future', null, {
|
||||
what: 'specifying recipients by email',
|
||||
value: item.value
|
||||
APIError.create('disallowed_value', null, {
|
||||
key: `recipients[${item.i}].type`,
|
||||
allowed: allowed_types,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -195,8 +200,44 @@ const v0_2 = async (req, res) => {
|
||||
|
||||
// Return: if there are invalid values in strict mode
|
||||
recipients_work.clear_invalid();
|
||||
|
||||
for ( const item of recipients_work.list() ) {
|
||||
if ( item.type !== 'email' ) continue;
|
||||
|
||||
const errors = [];
|
||||
if ( ! validator.isEmail(item.value) ) {
|
||||
errors.push('`email` is not valid');
|
||||
}
|
||||
|
||||
if ( errors.length ) {
|
||||
item.invalid = true;
|
||||
result.recipients[item.i] =
|
||||
APIError.create('field_errors', null, {
|
||||
key: `recipients[${item.i}]`,
|
||||
errors,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
recipients_work.clear_invalid();
|
||||
|
||||
// CHECK EXISTING USERS FOR EMAIL SHARES
|
||||
for ( const recipient_item of recipients_work.list() ) {
|
||||
if ( recipient_item.type !== 'email' ) continue;
|
||||
const user = await get_user({
|
||||
email: recipient_item.value,
|
||||
});
|
||||
if ( ! user ) continue;
|
||||
recipient_item.type = 'username';
|
||||
recipient_item.value = user.username;
|
||||
}
|
||||
|
||||
recipients_work.clear_invalid();
|
||||
|
||||
for ( const item of recipients_work.list() ) {
|
||||
if ( item.type !== 'username' ) continue;
|
||||
|
||||
const user = await get_user({ username: item.value });
|
||||
if ( ! user ) {
|
||||
item.invalid = true;
|
||||
@@ -243,8 +284,6 @@ const v0_2 = async (req, res) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log('thing?', thing);
|
||||
|
||||
const allowed_things = ['fs-share', 'app-share'];
|
||||
if ( ! allowed_things.includes(thing.$) ) {
|
||||
APIError.create('disallowed_thing', null, {
|
||||
@@ -417,7 +456,45 @@ const v0_2 = async (req, res) => {
|
||||
});
|
||||
|
||||
result.recipients[recipient_item.i] =
|
||||
{ $: 'api:status-report', statis: 'success' };
|
||||
{ $: 'api:status-report', status: 'success' };
|
||||
}
|
||||
|
||||
for ( const recipient_item of recipients_work.list() ) {
|
||||
if ( recipient_item.type !== 'email' ) continue;
|
||||
|
||||
const email = recipient_item.value;
|
||||
|
||||
// data that gets stored in the `data` column of the share
|
||||
const data = {
|
||||
$: 'internal:share',
|
||||
$v: 'v0.0.0',
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
for ( const share_item of shares_work.list() ) {
|
||||
data.permissions.push(share_item.permission);
|
||||
}
|
||||
|
||||
// track: scoping iife
|
||||
const share_token = await (async () => {
|
||||
const share_uid = await svc_share.create_share({
|
||||
issuer: actor,
|
||||
email,
|
||||
data,
|
||||
});
|
||||
return svc_token.sign('share', {
|
||||
$: 'token:share',
|
||||
$v: 'v0.0.0',
|
||||
uid: share_uid,
|
||||
});
|
||||
})();
|
||||
|
||||
const email_link = config.origin +
|
||||
`/sharelink?token=${share_token}`;
|
||||
|
||||
await svc_email.send_email({ email }, 'share_by_email', {
|
||||
link: email_link,
|
||||
});
|
||||
}
|
||||
|
||||
result.status = 'success';
|
||||
|
||||
@@ -46,6 +46,7 @@ const WHOAMI_GET = eggspress('/whoami', {
|
||||
username: req.user.username,
|
||||
uuid: req.user.uuid,
|
||||
email: req.user.email,
|
||||
unconfirmed_email: req.user.email,
|
||||
email_confirmed: req.user.email_confirmed,
|
||||
requires_email_confirmation: req.user.requires_email_confirmation,
|
||||
desktop_bg_url: req.user.desktop_bg_url,
|
||||
|
||||
@@ -141,6 +141,10 @@ If this was not you, please contact support@puter.com immediately.
|
||||
<p>Puter</p>
|
||||
`
|
||||
},
|
||||
'share_by_email': {
|
||||
subject: 'share by email',
|
||||
html: `testing: {{link}}`
|
||||
},
|
||||
}
|
||||
|
||||
class Emailservice extends BaseService {
|
||||
|
||||
@@ -28,7 +28,7 @@ class GetUserService extends BaseService {
|
||||
async get_user (options) {
|
||||
const user = await this.get_user_(options);
|
||||
if ( ! user ) return null;
|
||||
|
||||
|
||||
const svc_whoami = this.services.get('whoami');
|
||||
await svc_whoami.get_details({ user }, user);
|
||||
return user;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const { whatis } = require("../util/langutil");
|
||||
const { Actor, UserActorType } = require("./auth/Actor");
|
||||
const BaseService = require("./BaseService");
|
||||
const { DB_WRITE } = require("./database/consts");
|
||||
|
||||
class ShareService extends BaseService {
|
||||
static MODULES = {
|
||||
uuidv4: require('uuid').v4,
|
||||
validator: require('validator'),
|
||||
};
|
||||
|
||||
async _init () {
|
||||
this.db = await this.services.get('database').get(DB_WRITE, 'share');
|
||||
}
|
||||
|
||||
async create_share ({
|
||||
issuer,
|
||||
email,
|
||||
data,
|
||||
}) {
|
||||
const require = this.require;
|
||||
const validator = require('validator');
|
||||
|
||||
// track: type check
|
||||
if ( typeof email !== 'string' ) {
|
||||
throw new Error('email must be a string');
|
||||
}
|
||||
// track: type check
|
||||
if ( whatis(data) !== 'object' ) {
|
||||
throw new Error('data must be an object');
|
||||
}
|
||||
|
||||
// track: adapt
|
||||
issuer = Actor.adapt(issuer);
|
||||
// track: type check
|
||||
if ( ! (issuer instanceof Actor) ) {
|
||||
throw new Error('expected issuer to be Actor');
|
||||
}
|
||||
|
||||
// track: actor type
|
||||
if ( ! (issuer.type instanceof UserActorType) ) {
|
||||
throw new Error('only users are allowed to create shares');
|
||||
}
|
||||
|
||||
if ( ! validator.isEmail(email) ) {
|
||||
throw new Error('invalid email');
|
||||
}
|
||||
|
||||
const uuid = this.modules.uuidv4();
|
||||
|
||||
await this.db.write(
|
||||
'INSERT INTO `share` ' +
|
||||
'(`uid`, `issuer_user_id`, `recipient_email`, `data`) ' +
|
||||
'VALUES (?, ?, ?, ?)',
|
||||
[uuid, issuer.type.user.id, email, JSON.stringify(data)]
|
||||
);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ShareService,
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
this.db = new Database(this.config.path);
|
||||
|
||||
// Database upgrade logic
|
||||
const TARGET_VERSION = 11;
|
||||
const TARGET_VERSION = 12;
|
||||
|
||||
if ( do_setup ) {
|
||||
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
|
||||
@@ -60,6 +60,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
'0011_notification.sql',
|
||||
'0012_appmetadata.sql',
|
||||
'0013_protected-apps.sql',
|
||||
'0014_share.sql',
|
||||
].map(p => path_.join(__dirname, 'sqlite_setup', p));
|
||||
const fs = require('fs');
|
||||
for ( const filename of sql_files ) {
|
||||
@@ -120,6 +121,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
upgrade_files.push('0013_protected-apps.sql');
|
||||
}
|
||||
|
||||
if ( user_version <= 11 ) {
|
||||
upgrade_files.push('0014_share.sql');
|
||||
}
|
||||
|
||||
if ( upgrade_files.length > 0 ) {
|
||||
this.log.noticeme(`Database out of date: ${this.config.path}`);
|
||||
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE `share` (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"uid" TEXT NOT NULL UNIQUE,
|
||||
"issuer_user_id" INTEGER NOT NULL,
|
||||
"recipient_email" TEXT NOT NULL,
|
||||
"data" JSON DEFAULT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY ("issuer_user_id") REFERENCES "user" ("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
Reference in New Issue
Block a user