diff --git a/.meteor/packages b/.meteor/packages index fe1d901..fe1b6aa 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -22,3 +22,4 @@ react-meteor-data # React higher-order component for reactively tracking M roles@1.0.1 accounts-password@3.0.3 react-meteor-accounts +ostrio:files diff --git a/.meteor/release b/.meteor/release index eaae1a4..5f22892 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@3.1.1 +METEOR@3.1.2 diff --git a/.meteor/versions b/.meteor/versions index 6ea1169..3df2ca5 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -23,7 +23,7 @@ ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.2 ecmascript-runtime-server@0.11.1 ejson@1.1.4 -email@3.1.1 +email@3.1.2 es5-shim@4.8.1 facts-base@1.0.2 fetch@0.1.5 @@ -42,7 +42,7 @@ minifier-js@3.0.1 minimongo@2.0.2 mobile-experience@1.1.2 mobile-status-bar@1.1.1 -modern-browsers@0.1.11 +modern-browsers@0.2.0 modules@0.20.3 modules-runtime@0.13.2 modules-runtime-hot@0.14.3 @@ -52,6 +52,8 @@ mongo-dev-server@1.1.1 mongo-id@1.0.9 npm-mongo@6.10.2 ordered-dict@1.2.0 +ostrio:cookies@2.8.1 +ostrio:files@3.0.0-beta.6 promise@1.0.0 random@1.2.2 rate-limit@1.1.2 @@ -73,5 +75,5 @@ static-html-tools@1.0.0 tracker@1.3.4 typescript@5.6.3 url@1.3.5 -webapp@2.0.4 +webapp@2.0.5 webapp-hashing@1.1.2 diff --git a/imports/api/files.js b/imports/api/files.js new file mode 100644 index 0000000..2a563cd --- /dev/null +++ b/imports/api/files.js @@ -0,0 +1,275 @@ +import { Meteor } from 'meteor/meteor'; +import { FilesCollection } from 'meteor/ostrio:files'; +import { createBucket } from '/imports/api/lib/grid/createBucket.js'; +import { createObjectId } from '/imports/api/lib/grid/createObjectId.js'; +import fs from 'fs'; + +// import { Mongo } from 'meteor/mongo'; + +let filesBucket; + +if (Meteor.isServer) { + filesBucket = createBucket('allFiles'); +} + +const FilesCol = new FilesCollection({ + collectionName: 'Files', + allowClientCode: true, + debug: Meteor.isServer && process.env.NODE_ENV === 'development', + onBeforeUpload (file) { + // if (file.size <= 10485760 && /png|jpg|jpeg/i.test(file.extension)) { + // return true; + // } + // return 'Please upload image, with size equal or less than 10MB'; + return true; + }, + + onAfterUpload(file) { + const self = this; + + // here you could manipulate your file + // and create a new version, for example a scaled 'thumbnail' + // ... + + console.log("file_versions: ", file.versions); + + // then we read all versions we have got so far + Object.keys(file.versions).forEach(versionName => { + const metadata = { ...file.meta, versionName, fileId: file._id }; + fs.createReadStream(file.versions[ versionName ].path) + + // this is where we upload the binary to the bucket + .pipe(filesBucket.openUploadStream( + file.name, + { + contentType: file.type || 'binary/octet-stream', + metadata + } + )) + + // and we unlink the file from the fs on any error + // that occurred during the upload to prevent zombie files + .on('error', async err => { + console.error(err); + self.unlink(await this.collection.findOneAsync(file._id), versionName); // Unlink files from FS + }) + + // once we are finished, we attach the gridFS Object id on the + // FilesCollection document's meta section and finally unlink the + // upload file from the filesystem + .on('finish', Meteor.bindEnvironment(async ver => { + const property = `versions.${versionName}.meta.gridFsFileId`; + await self.collection.updateAsync(file._id, { + $set: { + [ property ]: ver._id.toHexString() + } + }); + self.unlink(await this.collection.findOneAsync(file._id), versionName); // Unlink files from FS + })) + }) + }, + + interceptDownload (http, file, versionName) { + const { gridFsFileId } = file.versions[ versionName ].meta || {}; + if (gridFsFileId) { + const gfsId = createObjectId({ gridFsFileId }); + const readStream = filesBucket.openDownloadStream(gfsId); + readStream.on('data', (data) => { + http.response.write(data); + }) + + readStream.on('end', () => { + http.response.end('end'); + }) + + readStream.on('error', () => { + // not found probably + // eslint-disable-next-line no-param-reassign + http.response.statusCode = 404; + http.response.end('not found'); + }) + + http.response.setHeader('Cache-Control', this.cacheControl); + http.response.setHeader('Content-Disposition', `inline; filename="${file.name}"`); + } + return Boolean(gridFsFileId) // Serve file from either GridFS or FS if it wasn't uploaded yet + }, + + onAfterRemove (files) { + files.forEach(file => { + Object.keys(file.versions).forEach(versionName => { + const gridFsFileId = (file.versions[ versionName ].meta || {}).gridFsFileId; + if (gridFsFileId) { + const gfsId = createObjectId({ gridFsFileId }); + filesBucket.deleteAsync(gfsId, err => { if (err) console.error(err); }); + } + }); + }); + } + +}); + +// if (Meteor.isClient) { +// Meteor.subscribe('files.all'); +// } + +if (Meteor.isServer) { + + Meteor.publish('files.all', () => { + return FilesCol.collection.find({userId: Meteor.userId()}); + }); + + Meteor.publish('avatars.all', () => { + return Avatars.collection.find({}); +}); + + // Meteor.publish('files.avatar', () => { + // return FilesCol.collection.find({ + // userId: Meteor.userId(), + // 'meta.type': 'avatar' + // }); + // }); + + // Meteor.publish('files.avatarRegister', (avatarId) => { + // if (avatarId) { + // if (!Meteor.userId()) { + // return FilesCol.collection.find({_id: avatarId}); + // } + // } else { + // return []; + // } + // }); + + // Meteor.publish('files.folderTree', () => { + // //if (upId) { + // if (Meteor.userId()) { + // return FilesCol.collection.find({ + // userId: Meteor.userId(), + // "meta.type": "folderTree" + // }); + // } else { + // return []; + // } + // }); + + // // files.cartellEventUpload + // Meteor.publish('files.cartellEventUpload', (cartellId) => { + // if (cartellId) { + // //if (!Meteor.userId()) { + // return FilesCol.collection.find({_id: cartellId}); + // } else { + // return []; + // } + // }); +} + +Meteor.methods({ + + 'RenameFile'(data){ + // if (!Meteor.userId()){ + // throw new Meteor.Error('not-authorized'); + // } + FilesCol.insertAsync({ + ...data, + createdAt: new Date(), + user: Meteor.userId() + }); + }, + + 'ReassignaUserIdFile'(userIdProvisional, uid){ + // if (!Meteor.userId()){ + // throw new Meteor.Error('not-authorized'); + // } + + FilesCol.collection.updateAsync({_id: userIdProvisional}, { + $set: { + meta: { + userId: uid + } + } + }); + }, + + 'RemoveFile'(fileToRemoveId) { + FilesCol.collection.removeAsync(fileToRemoveId); + } + + + // getFolderTreeFiles() { + // return FilesCol.find({ + // // userId: Meteor.userId(), + // // 'meta.type': 'folderTree' + // }).fetch(); + // } + // 'dates.update'(data){ + // // if (Meteor.userId() !== allcod.user){ + // // throw new Meteor.Error('not-authorized'); + // // } + // DatesCollection.update(data._id, { + // $set: { + // ...data + // } + // }); + // }, + + // 'dates.delete'(data){ + // // if (Meteor.userId() !== allcod.user){ + // // throw new Meteor.Error('not-authorized'); + // // } + // DatesCollection.remove(data._id); + // }, + + // 'dates.remove'(id, context) { + // DatesCollection.remove(id); + // } + +}); + + +// Create a new instance of the FilesCollection +const Avatars = new FilesCollection({ + collectionName: 'Avatars', + storagePath: 'assets/avatarStorage', + downloadRoute: '/avatar', + permissions: 0o755, + cacheControl: 'public, max-age=31536000', + allowClientCode: false, // Disallow remove files from Client +}); + +Meteor.methods({ + + 'registraUsuariAmbAvatar'(username, email, password, avatar) { + // Check if the username and email are valid + if (!username || !email || !password) { + throw new Meteor.Error('invalid-input', 'Please fill in all fields'); + } + + // Check if the avatar is a valid file + if (!avatar || !avatar.file) { + throw new Meteor.Error('invalid-avatar', 'Please select a valid avatar image'); + } + + // Upload the avatar to GridFS + const avatarId = Avatars.insert(avatar.file, (err, fileObj) => { + if (err) { + throw new Meteor.Error('avatar-upload-failed', 'Failed to upload avatar'); + } + }); + + // Create the new user + const userId = Accounts.createUser({ + username, + email, + password, + profile: { + avatar: avatarId, + }, + }); + + // Return the new user's ID + return userId; + } + +}); + +export { FilesCol, Avatars }; \ No newline at end of file diff --git a/imports/api/lib/grid/createBucket.js b/imports/api/lib/grid/createBucket.js new file mode 100644 index 0000000..5de873f --- /dev/null +++ b/imports/api/lib/grid/createBucket.js @@ -0,0 +1,6 @@ +import { MongoInternals } from 'meteor/mongo'; + +export const createBucket = bucketName => { + const options = bucketName ? {bucketName} : (void 0); + return new MongoInternals.NpmModule.GridFSBucket(MongoInternals.defaultRemoteCollectionDriver().mongo.db, options); +} diff --git a/imports/api/lib/grid/createObjectId.js b/imports/api/lib/grid/createObjectId.js new file mode 100644 index 0000000..079b845 --- /dev/null +++ b/imports/api/lib/grid/createObjectId.js @@ -0,0 +1,3 @@ +import { MongoInternals } from 'meteor/mongo' + +export const createObjectId = ({gridFsFileId}) => new MongoInternals.NpmModule.ObjectID(gridFsFileId); diff --git a/imports/api/lib/images.js b/imports/api/lib/images.js new file mode 100644 index 0000000..4f9fbec --- /dev/null +++ b/imports/api/lib/images.js @@ -0,0 +1,110 @@ +import { Meteor } from 'meteor/meteor'; +import { FilesCollection } from 'meteor/ostrio:files'; +import { createBucket } from '/imports/api/lib/grid/createBucket.js'; +import { createObjectId } from '/imports/api/lib/grid/createObjectId.js'; +import fs from 'fs'; + +let imagesBucket; +if (Meteor.isServer) { + imagesBucket = createBucket('allImages'); +} + +const Images = new FilesCollection({ + collectionName: 'Images', + allowClientCode: true, + debug: Meteor.isServer && process.env.NODE_ENV === 'development', + onBeforeUpload (file) { + if (file.size <= 10485760 && /png|jpg|jpeg/i.test(file.extension)) { + return true; + } + return 'Please upload image, with size equal or less than 10MB'; + }, + onAfterUpload (file) { + const self = this; + + // here you could manipulate your file + // and create a new version, for example a scaled 'thumbnail' + // ... + + // then we read all versions we have got so far + Object.keys(file.versions).forEach(versionName => { + const metadata = { ...file.meta, versionName, fileId: file._id }; + fs.createReadStream(file.versions[ versionName ].path) + + // this is where we upload the binary to the bucket + .pipe(imagesBucket.openUploadStream( + file.name, + { + contentType: file.type || 'binary/octet-stream', + metadata + } + )) + + // and we unlink the file from the fs on any error + // that occurred during the upload to prevent zombie files + .on('error', err => { + console.error(err); + self.unlink(this.collection.findOne(file._id), versionName); // Unlink files from FS + }) + + // once we are finished, we attach the gridFS Object id on the + // FilesCollection document's meta section and finally unlink the + // upload file from the filesystem + .on('finish', Meteor.bindEnvironment(ver => { + const property = `versions.${versionName}.meta.gridFsFileId`; + self.collection.update(file._id, { + $set: { + [ property ]: ver._id.toHexString() + } + }); + self.unlink(this.collection.findOne(file._id), versionName); // Unlink files from FS + })) + }) + }, + interceptDownload (http, file, versionName) { + const { gridFsFileId } = file.versions[ versionName ].meta || {}; + if (gridFsFileId) { + const gfsId = createObjectId({ gridFsFileId }); + const readStream = imagesBucket.openDownloadStream(gfsId); + readStream.on('data', (data) => { + http.response.write(data); + }) + + readStream.on('end', () => { + http.response.end('end'); + }) + + readStream.on('error', () => { + // not found probably + // eslint-disable-next-line no-param-reassign + http.response.statusCode = 404; + http.response.end('not found'); + }) + + http.response.setHeader('Cache-Control', this.cacheControl); + http.response.setHeader('Content-Disposition', `inline; filename="${file.name}"`); + } + return Boolean(gridFsFileId) // Serve file from either GridFS or FS if it wasn't uploaded yet + }, + onAfterRemove (files) { + files.forEach(file => { + Object.keys(file.versions).forEach(versionName => { + const gridFsFileId = (file.versions[ versionName ].meta || {}).gridFsFileId; + if (gridFsFileId) { + const gfsId = createObjectId({ gridFsFileId }); + imagesBucket.delete(gfsId, err => { if (err) console.error(err); }); + } + }); + }); + } +}); + +if (Meteor.isClient) { + Meteor.subscribe('files.images.all'); +} + +if (Meteor.isServer) { + Meteor.publish('files.images.all', () => Images.collection.find({})); +} + +export { Images }; diff --git a/imports/api/lib/persons.js b/imports/api/lib/persons.js new file mode 100644 index 0000000..4e1e37d --- /dev/null +++ b/imports/api/lib/persons.js @@ -0,0 +1,62 @@ +import { Mongo } from 'meteor/mongo'; +import { Images } from './images.js'; +// import SimpleSchema from 'simpl-schema'; + +// SimpleSchema.extendOptions(['autoform']); +// SimpleSchema.setDefaultMessages({ +// initialLanguage: 'en', +// messages: { +// en: { +// uploadError: '{{{value}}}' +// } +// } +// }); + +const Persons = new Mongo.Collection('persons'); + +Persons.helpers({ + profilePic() { + return Images.find({_id: this.profilePicId}); + }, + backgroundPic() { + return Images.find({_id: this.backgroundPicId}); + } +}); + +// Persons.attachSchema({ +// name: { +// type: String, +// label: 'Name' +// }, +// profilePicId: { +// type: String, +// label: 'Profile Pic Id', +// autoform: { +// afFieldInput: { +// type: 'fileUpload', +// collection: 'Images', +// insertConfig: { +// transport: 'http' +// }, +// uploadTemplate: 'uploadField', // <- Optional +// previewTemplate: 'uploadPreview', // <- Optional, +// } +// } +// }, +// backgroundPicId: { +// type: String, +// label: 'Background Pic Id', +// autoform: { +// afFieldInput: { +// type: 'fileUpload', +// collection: 'Images', +// accept: '.png,.jpg,.jpeg', // use built-in accept config +// insertConfig: { +// transport: 'http' +// } +// } +// } +// } +// }); + +export { Persons }; diff --git a/imports/ui/BarraNav/UserStat.jsx b/imports/ui/BarraNav/UserStat.jsx index eace1b1..ac2526b 100644 --- a/imports/ui/BarraNav/UserStat.jsx +++ b/imports/ui/BarraNav/UserStat.jsx @@ -47,7 +47,7 @@ const useLongPress = (mostraMenu, setMostraMenu) => { if (isPressed) { setIsPressed(false); // Clear any pending timeout - clearTimeout(timeoutId?.current); + // clearTimeout(timeoutId?.current); } }; @@ -69,6 +69,7 @@ const useLongPress = (mostraMenu, setMostraMenu) => { } }, 500); + // return () => clearTimeout(timeoutId); } return () => clearTimeout(timeoutId); @@ -248,7 +249,7 @@ const UserStat = ({esAdministrador, setEsAdministrador}) => { left: 0, top: `5em`, padding: `.4em .5em`, - border: `1px #aaa`, + border: `1px solid #aaa`, cursor: `pointer`, zIndex: `200` }} diff --git a/imports/ui/Login.tsx b/imports/ui/Login.tsx index 16d7f9b..35c44e0 100644 --- a/imports/ui/Login.tsx +++ b/imports/ui/Login.tsx @@ -1,14 +1,46 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Accounts } from 'meteor/accounts-base'; import { useNavigate } from 'react-router-dom'; import { Meteor } from 'meteor/meteor'; +import { useTracker, useSubscribe, useFind } from 'meteor/react-meteor-data/suspense'; import { Roles } from 'meteor/roles'; import { ROLS_GLOBALS } from '../roles'; +import { Avatars } from '/imports/api/files.js'; +import AvatarFileUpload from '/imports/ui/files/AvatarFileUpload'; + export const Login = () => { const [isLogin, setIsLogin] = useState( { initialState: true } ); const navigate = useNavigate(); + // const avatarinput = useRef(); + + // const [avatarId, setAvatarId] = useState(); + // const [avatarLink, setAvatarLink] = useState(); + + // const [novaImg, setNovaImg] = useState(false); + + // const [avatar, setAvatar] = useState(null); + + // let files; + + // useEffect(() => { + + // useSubscribe('avatars.all'); + + // files = useFind(Avatars, [ + // {}, + // { sort: { createdAt: -1 } }, + // ]); + + // }, []); + + // const files = useTracker("avatars", () => { + // return Avatars.find({}).fetchAsync(); + // }); + + + const handleLogin = (e) => { e.preventDefault(); // console.dir(e); @@ -74,6 +106,8 @@ export const Login = () => { } + + return (