Skip to content

2024

How to use a FireBase as a Auth provider (untested)

Angular

Install firebase to angular

npm install firebase @angular/fire

Initialize firebase in Angular app

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
};

const app = initializeApp(firebaseConfig);

Log user in

import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";

const auth = getAuth();
const provider = new GoogleAuthProvider();

signInWithPopup(auth, provider)
  .then((result) => {
    const user = result.user;
    console.log("User signed in:", user);
  })
  .catch((error) => {
    console.error("Error during sign-in:", error);
  });

Send the id to SpringBoot

const user = auth.currentUser;
if (user) {
  user.getIdToken().then((idToken) => {
    // Send idToken to backend for verification
    this.http.post('/api/authenticate', { token: idToken }).subscribe();
  });
}

SpringBoot

Configure Firebase Admin SDK dependency

<dependency>
  <groupId>com.google.firebase</groupId>
  <artifactId>firebase-admin</artifactId>
  <version>8.1.0</version>
</dependency>

Initialize Firebase in SpringBoot

import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.auth.oauth2.GoogleCredentials;

@PostConstruct
public void initFirebase() {
    FirebaseOptions options = FirebaseOptions.builder()
        .setCredentials(GoogleCredentials.fromStream(new FileInputStream("path/to/serviceAccountKey.json")))
        .build();
    FirebaseApp.initializeApp(options);
}

Verify the token in a controller

import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthException;
import com.google.firebase.auth.FirebaseToken;

@PostMapping("/api/authenticate")
public ResponseEntity<?> authenticate(@RequestBody Map<String, String> body) {
    try {
        String idToken = body.get("token");
        FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(idToken);
        String uid = decodedToken.getUid();

        // Perform additional logic (e.g., store/retrieve user data from MySQL)
        return ResponseEntity.ok("User authenticated: " + uid);
    } catch (FirebaseAuthException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token");
    }
}

Using Ubuntu Snap Store

Download Immich Distribution. When finished it will run on port 80, i.e. localhost. The persisted locations are found in

/var/snap/immich-distribution/common

i.e. you'll see the pgsql and upload folder here.

To see the snap variables run

snap get immich-distribution

If you want to change the port run

sudo snap set immich haproxy-http-bind="*:2283"

Then restart

sudo snap restart immich-distribution

To stop

sudo snap stop immich-distribution

More info here https://immich-distribution.nsg.cc/install/

Using Umbrel

Installing CasaOS in Ubuntu

Where does app data reside

The app data for immich resides in /DATA/AppData/big-bear-immich.

In here we have the pgdata folder and upload folder.

Mounting a SMB network drive.

If you want to map a NAS to store your immich assets, you can mount your network drive.

sudo apt update
sudo apt install cifs-utils
sudo mkdir /mnt/smb_share
sudo mount -t cifs //server_ip/share /mnt/smb_share -o username=your_username,password=your_password

For me this would be

sudo mount -t cifs //192.168.4.142/raid /mnt/smb_share -o username=thomas,password=<password>

Let's say we want to change the upload location

mv -v /DATA/AppData/big-bear-immich/upload /home/thomas/

Then change the environmental variable in immich.

I could have also mounted the upload location to my smb network drive.

mv -v /home/thomas/upload /mnt/smb_share/casaos/immich

If I wanted to copy a folder from one location to another (i.e. backup), I can do

cp -r /source/path /destination/path

Using Umbrel

Installing UmbrelOS in VM

Using OracleVM, set up a new machine as usual. You will need to manually select Linux and Oracle Linux (64-bit) and also be sure to Enable EFI.

After installing the medium to the disk the machine will turn off. Change the boot order ot only include the hard disk. Go to storage and click on the installation disk. There is an option to eject the disk on the far right.

You can then navigate to umbrel.local in the browser.

Where does app data reside

The app data resides in /home/umbrewl/umbrel/app-data.

So immich is located in /home/umbrewl/umbrel/app-data/immich for example and in this directory you can see the docker-compose.yml file and the data directory which has the persisted database and assets upload library.

Mounting a SMB network drive.

If you want to map a NAS to store your immich assets, you can mount your network drive.

sudo apt update
sudo apt install cifs-utils
sudo mkdir /mnt/smb_share
sudo mount -t cifs //server_ip/share /mnt/smb_share -o username=your_username,password=your_password

What is Immich?

Immich is a self-hosted, full stack, containerized application that is effectively a drop-in replacement for Google Photos and Apple Photos.

Why would I want this

You have a lot of photos and videos and you don't want to pay subscription fees to Google and Apple for the rest of eternity. You want to take control of your own cloud services and own your own data again!

What do I need to host Immich

You need some hardware and storage. You can get started with what you already have, i.e. a laptop or a desktop. In this setup, you can boot up your hardware whenever you want to view photos or sync photos from your mobile apps. Eventually, you'll want to upgrade to a dedicated machine that is always on. This can be as simple as a raspberry pi to run the app or as complicated as a used enterprise server.

How can I "test" Immich out before purchasing additional hardware

I'm assuming you have a Windows or Mac device. You can run Immich in Docker Desktop which is able to run docker containers on your OS with a intuitive user interface. If you want to continue this approach and be able to access your website from the web and sync your phone app with your Immich running in Docker desktop, you can install TailScale on both devices or set up a CloudFlare tunnel in another docker container.

If you want to test running Immich on an actual server, you can then create a Virtual Machine on your computer using Oracle VM Virtual Box. This allows you to test any configuration you want on a server that you can clone or destroy at any time.

Setting up Immich seems complicated, why isn't it easier?

You have to remember, Google Photos isn't just a front end web site. It has a database running on a server, it has microservices that need to run object detection and facial recognition and update that meta data on the database. It has an API for the front end applications on web and mobile to fetch and sync images.

This is why we use Docker. Docker allows us to run a "container" with a simple build script outline in a docker-compose.yml file that will basically pull the images of each of these necessary applications from the docker hub repository, and start them up in a docker container which is basically like an optimized Virtual Server. It's pretty amazing really.

I have a dedicated machine to run Immich on, now what?

There are essentially three different ways to do this.

Normal Linux OS, Docker Install

Run a normal operating system like Ubuntu or Fedora. Install docker, and install docker with docker compose.

Normal Linux OS, us a PaaS

Same as above, but install a Platform as a Service to manage Immich. This can be something relatively simple like Portainer. Or something even more hands off like Cloudron.

Home Server OS with 'App Store'

In this case, we're talking about installing an OS that is dedicated to streamlining the home server experience. This would be something like Umbrel or CasaOS, where we can just navigate to the app store and basically click on Immich to be installed.

Serverless Relational Databases

Here are some services I should look into that would likely end up costing a lot less than something like RDS.

Service Compute Pricing Storage Pricing Ideal For
Amazon Aurora Serverless Billed per ACU per second. $0.10 per GB-month. Variable workloads with MySQL/PostgreSQL compatibility.
Azure SQL Database Serverless Billed per vCore per second; e.g., ~$0.5218 per vCore-hour for Gen5. Billed separately per GB. Intermittent workloads with auto-scaling and pausing capabilities.
Google Cloud SQL On-demand and committed use pricing; e.g., ~$0.0825 per hour for 1 vCPU instance. $0.17 per GB per month for SSD. Managed relational databases with automated scaling.
PlanetScale Free tier available; paid plans based on resource consumption. Included in usage-based pricing. MySQL-compatible databases with serverless scaling and modern development features.
CockroachDB Serverless Free tier available; usage-based pricing beyond free tier. Included in usage-based pricing. PostgreSQL-compatible distributed SQL databases requiring high availability and global scaling.

Creating a working JavaFX demo in the web

I did this working in my BasicDemo app.

I created my own Dockerfile and docker-compose.yml like the following

Dockerfile
FROM amazoncorretto:21
# Update yum and install dependencies
RUN yum update -y && yum install -y \
    pango \
    cairo \
    freetype \
    fontconfig && \
    yum clean all
# Set the working directory and configure the CMD
WORKDIR /jproserver
CMD ["sh", "-c", "cd /jproserver && ./bin/restart.sh"]

The installations are necessary to run the project in Docker.

Then the docker-compose.yml file:

docker-compose.yml
services:
  jpro-demo:
    container_name: jpro-demo
    build:
      context: .  # Directory where Dockerfile is located
      dockerfile: Dockerfile
    ports:
      - "7080:8080"
    volumes:
      - .:/jproserver
    restart: always

In my build.gradle I used an ssh plugin to deploy and run my application id 'org.hidetake.ssh' version '2.11.2'

I created two tasks cleanAll and deployHome to basically clean the build directory, down the docker app, and remove the persisted storage of the build. Then the deployHome moves the build zip, unzips it, then unpacks the parent folder.

plugins {
    id 'java'
    id 'application'
    id 'org.javamodularity.moduleplugin' version '1.8.12'
    id 'org.openjfx.javafxplugin' version '0.0.13'
    id 'org.beryx.jlink' version '2.25.0'
    id 'jpro-gradle-plugin'
    id 'org.hidetake.ssh' version '2.11.2'
}

/**
More Stuff...
*/

ssh.settings {
    knownHosts = file("${System.env.USERPROFILE}/.ssh/known_hosts")
//    knownHosts = allowAnyHosts
}

remotes {
    homeServer {
        host = home_ip // Replace with your instance IP
        user = home_user // Replace with your SSH username
        identity = file(home_pk) // Replace with the path to your private SSH key
    }
}

task cleanAll(dependsOn: 'clean') {
    doLast {
        ssh.run {
            session(remotes.homeServer) {
                // delete everything in the jpro folder
                execute 'docker compose -f /home/thomas/docker/jpro/docker-compose.yml down -v'
                execute 'rm -rf /home/thomas/docker/jpro/*'
            }
        }
    }
}

task deployHome(dependsOn: 'jproRelease') {
    doLast {
        ssh.run {
            session(remotes.homeServer) {
                put from: project.file("build/distributions/BasicDemo-jpro.zip"), into: '/home/thomas/docker/jpro/' // Replace with remote path
                // unzip the file on the remote server
                execute 'unzip /home/thomas/docker/jpro/BasicDemo-jpro.zip -d /home/thomas/docker/jpro/' // Replace with your actual service name

                // delete the zip file
                execute 'rm /home/thomas/docker/jpro/BasicDemo-jpro.zip'

                // Unpack and overwrite all the contents of the BasicDemo-jpro folder into the jpro folder
                execute 'cp -r /home/thomas/docker/jpro/BasicDemo-jpro/* /home/thomas/docker/jpro/' // Replace with your actual service name


                put from: project.file("Dockerfile"), into: '/home/thomas/docker/jpro/' // Replace with remote path
                put from: project.file("docker-compose.yml"), into: '/home/thomas/docker/jpro/' // Replace with remote path
                // Restart the service on the remote server
                execute 'docker compose -f /home/thomas/docker/jpro/docker-compose.yml up -d --build' // Replace with your actual service name
            }
        }
    }
}

Flutter Docs Android

Changes in build.gradle in the android directory

ext.kotlin_version = '1.8.10' 

and

classpath 'com.android.tools.build:gradle:8.1.0'

I also changed the gradle-wrapper.properties to version 8.1

I also manually set the JDK version to 17 (default 21) in gradle.properties

org.gradle.java.home=/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home

Flutter Firebase Tutorial

YouTube tutorial (https://www.youtube.com/watch?v=iQOvD0y-xnw)

Install firebase-tools cli if you haven't already.

npm install -g firebase-tools

Login to the Firebase CLI

firebase login

Activate the flutterfire cli

flutter pub global activate flutterfire_cli

Configure the project with a firebase project

flutterfire configure

Add dependencies

flutter pub add firebase_core

I had an issue with cocopods, fixed it by reinstalling with brew (https://stackoverflow.com/questions/62593939/cocoapods-not-installed-or-not-in-valid-state)

sudo gem uninstall cocoapods
brew install cocoapods
brew link --overwrite cocoapods
brew unlink cocoapods && brew link cocoapods
fire_base core error

Error: The pod "Firebase/CoreOnly" required by the plugin "firebase_core" requires a higher minimum iOS deployment version than the plugin's reported minimum version. To build, remove the plugin "firebase_core", or contact the plugin's developers for assistance. Error running pod install

Open the ios/PodFile and uncomment the platform line, change to version '13.0'

Add firestore dependency for firestore database

flutter pub add cloud_firestore

To get debugging certificates SHA-1 to use google sign in when debugging

cd android/
./gradlew signingReport

I followed the instructions for setting up android, and ended up getting an error;

Following this stackoverflow (https://stackoverflow.com/questions/74226250/error-member-not-found-firebaseappplatform-verifyextends) this seemed to fix it.

flutter pub upgrade --major-versions

I was then warned that the min sdk needed to be 23, so I manually set that value from the original flutterMinSdkVersion

Fireship.io tutorial

To create new project

flutter create --org com.wildetechsolutions myapp

To check flutters compatibility with all build modes, run

flutter doctor --verbose

Can write flutter widgets in the browser at https://dartpad.dev/

Most basic element is a Container, like a div in html.

Right click on widget, refactor to another type of widget.

Flutter provides row and column widgets.

Stack widget, like a StackPane.

Hero widget can animate between pages.

Future one-teim async value (single doc read) Stream multipel async values (listen to real-time updates)

Stateful widget cons

  • sharing data with children
  • mixing logic with UI

provider package helps to separate the business logic from the widget and provide data to stateless widgets throughout the widget tree

Setup

So the Firebase setup video does seem a bit out of date, the code is very different than the setup provided at (https://firebase.flutter.dev/docs/overview/). In fact, the setup seems a lot more similar to the process followed on my 12-14 blog post.

Flutter gives two options for routing:

Navigator 1.0 (push and pop screens to a stack)

and

Navigator 2.0 (hasn't been well received)

Theming

This has changed a bit from the tutorial and some of it is deprecated. Concepts make sense though in defining custom theme in theme.dart

Home Login

Using a stream building, we can allow the authentication state dictate where the app goes from our default home.dart path

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: AuthService().userStream,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Text("loading");
        } else if (snapshot.hasError) {
          return const Center(
            child: Text("error"),
          );
        } else if (snapshot.hasData) {
          return const TopicsScreen();
        } else {
          return const LoginScreen();
        }
      },
    );
  }
}

With our initial auth service, the user will not be logged in, and therefore routed to the LoginScreen

auth.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
  final userStream = FirebaseAuth.instance.authStateChanges(); // asynchronous stream to listen to the user's authentication state
  final user = FirebaseAuth.instance.currentUser; // synchronous method to get the current user
}

Authentication

In the firebase console, go to the project then go to Authentcation. Hi getting started. Go to sign-in method and enable Anonymous as a provider. This will allow a user to use the app without being registed and we'll still get a user id for them.

Add Google Sign in as a provider on firebase. It will then give you an updated json file to include in the android project. I needed to stop the app, and start it again for the json file to take effect. Hot restart did't work initially. However, after starting the app, the google sign in did work.

Going to skip Apple auth until I set it up on the mac. Note that Apple Auth is required if other social login methods are included.

Database data population

After cloning the repository I did need to run

npm install @google-cloud/firestore

in order for the node scripts to work.

Creating the FireStore database was kind of glossed over. But simply go to the FireStore database and hit create database, I set it to production mode. The node.js project to populate data worked as expected.

Models

Bring in json_serializeable, build_runner dependency into dev_dependencies which facilitates serializing/deserializing objects to json. Also bring json_annotation dependency into the normal dependencies section.

I added dependency and running flutter pub upgrade --major-versions to get it up to date (json_serializable: ^6.8.0 at the time of writing)

For each class you'll want to add this code

factory Topic.fromJson(Map<String, dynamic> json) => _$TopicFromJson(json);
Map<String, dynamic> toJson() => _$TopicToJson(this);

Now from the terminal you can just run the following to generate the mappers

flutter pub run build_runner build

Building UI

For dynamic UI rendering based on a returned api response, we can use FutureBuilder which takes the future as an argument and then a builder function which can return the widget based on the result of the future. You can also strongly type the FutureBuilder, i.e. FutureBuilder<List<Topic>>

Topics Screen build function
@override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Topic>>(
      future: FirestoreService().getTopics(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const LoadingScreen();
        } else if (snapshot.hasError) {
          return Center(
            child: ErrorMessage(message: snapshot.error.toString()),
          );
        } else if (snapshot.hasData) {
          var topics = snapshot.data!;

          return ...[widget];
        } else {
          return const Text('No topics found in Firestore. Check database');
        }
      },
    );
  }

Navigator does not necessarily have to navigate to a route, it can navigate to a widget as well

Navigator.of(context).push(
              MaterialPageRoute(
                builder: (BuildContext context) => TopicScreen(topic: topic),
              ),
            );

Provider

Provider is a very popoluate flutter library, easier to share values throughout the widget tree. We'll want this to listen to the report stream from multiple widgets. We could listen to it with a StreamBuilder for every page/widget, but that wouldn't be very efficient.