AlanJereb.com
Programmazione

Ciao: L'app Social Beaver va offline

Apr 22nd, 2024

L'esperimento dell'app sociale,

Social Beaver

, va offline dopo più di tre anni di utilizzo continuo e ininterrotto. Quello che è iniziato come un mini progetto per padroneggiare il contenitore di gestione dello stato di React Redux si è trasformato in una completa rete sociale simile a Twitter che funziona su Firebase.

Ambito di questo post

L'ambito di questo post non è né educativo né quello di dare una panoramica dettagliata del codice e spiegare tutte le funzionalità e le ragioni dietro la mia implementazione del codice.

No, terrò le cose brevi e semplici. Oggi tutti sanno come funzionano i social network e cosa ci si può aspettare usando uno. Non c'è bisogno per me di entrare nei dettagli. Il post è principalmente per me per ricordare la piattaforma e per chi legge per vedere cosa stavo facendo nei miei primi anni di codifica.

Nei paragrafi seguenti, menzionerò brevemente alcune delle principali funzionalità della piattaforma insieme alle loro schermate.

Se, per qualche motivo, decidi di arrivare fino alla fine, ti aspetta una sorpresa. Mostrerò come un semplice commento su una pubblicazione ha innescato una serie di cose sullo sfondo. Una spiegazione banale per i programmatori esperti, ma potrebbe essere interessante per tutti gli altri.

Di cosa era fatto Social Beaver?

Innanzitutto, devo menzionare che la piattaforma è stata programmata pensando prima di tutto ai dispositivi mobili. Volevo che fosse reattiva. Credo che al giorno d'oggi più persone utilizzino i social network sui loro dispositivi mobili piuttosto che sui loro computer fissi.

Registrazione/Login

Non è un social network senza utenti e non puoi avere utenti senza che creino un account e possano accedere con esso.

Una chiamata al servizio di autenticazione di Firebase sul backend, e voilà, l'utente è loggato. Non sono più un fan di Firebase, ma hanno reso davvero semplice per gli sviluppatori.

App completamente responsiva - pagina di autenticazione

App completamente responsiva - pagina di autenticazione

Homepage

Gli utenti potevano accedere alla homepage senza essere loggati, ma dovevano essere loggati per interagire con la piattaforma. Ciò è stato fatto sia per la sicurezza degli utenti che per il mio portafoglio.

I dati legati agli account potevano essere moderati rapidamente, e una moderazione veloce mi evitava mal di testa nel caso in cui Social Beaver diventasse un obiettivo di un attacco di spam di contenuti. Lo storage e le richieste diventano rapidamente costosi su Firebase. Ho effettivamente impostato il limite massimo mensile delle mie spese lì, ma preferisco evitarlo se possibile.

Homepage (versione desktop)

Homepage (versione mobile)

Profilo

Gli utenti potevano cambiare la loro foto, la biografia, la posizione e il link al loro sito web. Questo è solo una frazione di ciò che gli utenti dovrebbero essere in grado di fare con i loro profili, poiché non avevano alcun modo per cambiare la loro password, richiedere i loro dati utente o impostare una sicurezza aggiuntiva per l'autenticazione come la doppia autenticazione (2FA) e frasi anti-phishing, ecc.

Ma poiché questo era un progetto di apprendimento, senza che io avessi l'intenzione di spingerlo alle masse, puoi capire perché le funzionalità sopra citate mancano.

Editor del profilo

Editor del profilo

Visualizzazione dei post e commenti

Non sarebbe chiamato un social network se gli utenti non potessero creare post, leggere post ed esprimere le proprie opinioni sotto forma di commenti o mi piace. Non c'è davvero nulla di innovativo in queste funzionalità, ma dovevano essere menzionate per i motivi sopra indicati.

Aggiungi un commento

Aggiungi un commento

Un piccolo extra

Sei arrivato alla fine dell'articolo. Grazie! Come promesso, ti mostrerò cosa provoca in background un commento a un post.

La foto qui sopra ti mostra l'interfaccia utente per aggiungere un commento. Quando premi il pulsante di invio, il frontend invia una richiesta POST al backend.

export const submitComment = (biteId: string, commentData: string) => (dispatch: Dispatch) => {
    axios.post(`/bites/${biteId}/comment`, commentData)
        .then((res) => {
            dispatch({
                type: SUBMIT_COMMENT,
                payload: res.data,
            });
            dispatch<any>(clearErrors())
        })
        .catch((err) => {
            dispatch({
                type: SET_ERRORS,
                payload: err.response.data,
            });
        });
}

Invia un commento sull'interfaccia utente

Utilizzando Express.js, il backend ascolta gli eventi POST della route.

app.post('/bites/:biteId/comment', FBAuth, commentBite)

Endpoint per aggiungere un commento

Quando l'evento viene ricevuto sulla route monitorata, il middleware

FBAuth

si assicura prima che l'utente sia autenticato e possa inviare tali eventi.

export const FBAuth = (req: Request, res: Response, next: NextFunction) => {
    let idToken;
    if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
        idToken = req.headers.authorization.split('Bearer ')[1];
    } else {
        // 403 - unauthorized
        console.error('No token found');
        return res.status(403).json({ error: 'Unauthorized' }).end();
    }
    // verify token
    admin
        .auth()
        .verifyIdToken(idToken)
        .then((decodedToken) => {
            req.user = decodedToken;
            return db  
                .collection('users')
                .where('userId', '==', req.user.uid)
                .limit(1)
                .get()
        })
        .then((data) => {
            req.user.handle = data.docs[0].data().handle;
            req.user.imageUrl = data.docs[0].data().imageUrl;
            return next();
        })
        .catch((err) => {
            console.error('Error while verifying token ', err);
            return res.status(403).json(err);
        })
}

Middleware di autenticazione

Se l'autenticazione è riuscita, il middleware FBAuth aggiunge il nome utente e l'URL dell'immagine del profilo dell'utente alla richiesta e la inoltra alla funzione che aggiorna il database di Firebase (Firestore) con il commento appena aggiunto.

export const commentBite = (req: Request, res: Response) => {
    if (req.body.body.trim() === "") {
        return res.status(400).json({ comment: "Must not be empty"}).end();
    }
    const newComment: any = {
        body: req.body.body,
        userHandle: req.user?.handle,
        createdAt: new Date().toISOString(),
        biteId: req.params.biteId,
        userImage: req.user.imageUrl
    };
    db
        .doc(`bites/${newComment.biteId}`)
        .get()
        .then((doc) => {
            if (!doc.exists) {
                res.status(404).json({ error: "Bite not found" }).end();
            }
            return doc.ref.update({ commentCount: doc.data()!.commentCount + 1 })
        .then(() => {
            return db
                .collection('comments')
                .add(newComment)
            })
        })
        .then(() => {
            return res.json(newComment);
        })
        .catch((err) => {
            console.error(err);
            return res.status(500).json({ error: "Something went wrong" });
        });
}

Aggiunta di un commento al database

Infine, viene attivato un listener con un commento appena aggiunto, che, in risposta, invia una notifica per il commento ricevuto all'utente il cui post l'ha ricevuto.

export const createNotificationOnComment = functions
    .region('europe-west3')
    .firestore
    .document('comments/{id}')
    .onCreate((snapshot) => {
        return db
            .doc(`/bites/${snapshot.data().biteId}`)
            .get()
            .then((doc) => {
                if (doc.exists && doc.data()!.userHandle !== snapshot.data().userHandle) {
                    return db
                        .collection('notifications')
                        .doc(snapshot.id)
                        .set({
                            createdAt: new Date().toISOString(),
                            recipient: doc.data()!.userHandle,
                            sender: snapshot.data().userHandle,
                            type: "comment",
                            read: false,
                            biteId: doc.id
                        })
                }
                return;
            })
            .catch((err) => {
                console.error(err);
            })
    });

Aggiunta di notifica

A questo punto, un WebSocket potrebbe inviare un aggiornamento delle notifiche in tempo reale all'utente mentre riceve il commento e la notifica, ma per semplicità ho optato per non farlo.

Notifica di nuovo commento

Cliccando sulla notifica, viene visualizzato il commento

Quanto sopra è solo uno degli esempi. Molti listener vengono attivati in background. Ecco alcuni esempi:

  • Creazione di una notifica quando un utente mette mi piace a un post
  • Eliminazione di una notifica quando un utente rimuove il suo mi piace da un post
  • Aggiornare tutti i post con la nuova immagine del profilo utente quando l'autore cambia la sua foto del profilo
  • Aggiornare tutti i commenti con la nuova immagine del profilo utente quando l'autore cambia la sua foto del profilo
  • Eliminare la vecchia foto dallo storage quando un utente cambia la sua foto del profilo
  • Eliminare tutti i commenti, i mi piace e le notifiche quando un utente elimina un post
  • ...