Skip to content

Commit

Permalink
feat/spotify-account (#32)
Browse files Browse the repository at this point in the history
* feat(#2): connection with Spotify

* feat(backend): creation of account if not already & generate types

* refactor: formatting auth routing

* feat: auth automatism

* chore: some typo & added share Database type with monorepo

* chore: logical to force new user to choice username

* fix: spam database to get user-profile

* chore: improve some log message & UX with error handler

* fix: auth with Spotify provider in mobile

* fix: misstake with expo Screen

* feat(backend): added route /auth/redirection

* chore: improved error response

* chore: added other method to store supabase on client

* refactor(backend): improve lisibility of auth/callback

* chore(expo): refacto Alert & added scope of Spotify provider

* chore(backend): reponse to pull request

* chore(expo): added env variable to expo project with skeleton

* refactor(expo): improve code comprehension

* chore: removed unnecessary code
  • Loading branch information
GaspardBBY authored Jan 14, 2024
1 parent 0baa50f commit 7b655ba
Show file tree
Hide file tree
Showing 27 changed files with 1,458 additions and 36 deletions.
380 changes: 379 additions & 1 deletion backend/package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/cookie": "^9.2.0",
"@fastify/cors": "^8.5.0",
"@supabase/auth-helpers-nextjs": "^0.8.7",
"@supabase/ssr": "^0.0.10",
"@supabase/supabase-js": "^2.39.1",
"dotenv": "^16.3.1",
"fastify": "^4.25.2",
"fastify-socket.io": "^5.0.0",
"socket.io": "^4.7.3"
"socket.io": "^4.7.3",
"supabase": "^1.131.3"
},
"devDependencies": {
"@types/node": "^20.10.5",
Expand Down
38 changes: 38 additions & 0 deletions backend/src/lib/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createServerClient } from "@supabase/ssr";
import { FastifyReply, FastifyRequest } from "fastify";

export default function createClient(context: {
request: FastifyRequest;
response: FastifyReply;
}) {
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
throw new Error(
"Missing SUPABASE_URL or SUPABASE_ANON_KEY environment variable"
);
}
return createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{
cookies: {
get: (key: any) => {
const cookies = context.request.cookies;
const cookie = cookies[key] ?? "";
return decodeURIComponent(cookie);
},
set: (key: any, value: any, options: any) => {
if (!context.response) return;
context.response.cookie(key, encodeURIComponent(value), {
...options,
sameSite: "Lax",
httpOnly: true,
});
},
remove: (key: any, options: any) => {
if (!context.response) return;
context.response.cookie(key, "", { ...options, httpOnly: true });
},
},
}
);
}
167 changes: 167 additions & 0 deletions backend/src/route/AuthCallbackGET.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { FastifyRequest, FastifyReply } from "fastify";
import createClient from "../lib/supabase";
import { adminSupabase } from "../server";
import { Database } from "../types/dbTypes";
import { PostgrestError } from "@supabase/supabase-js";

enum StreamingService {
Spotify = "a2d17b25-d87e-42af-9e79-fd4df6b59222",
SoundCloud = "c99631a2-f06c-4076-80c2-13428944c3a8",
}

export default async function AuthCallbackGET(
request: FastifyRequest,
response: FastifyReply
) {
const supabaseUrl = process.env.SUPABASE_URL;
if (!supabaseUrl)
return response.code(400).send({ error: "Missing SUPABASE_URL" });
const supabaseId = supabaseUrl.split(".")[0].split("//")[1];

if (!request.cookies["sb-" + supabaseId + "-auth-token-code-verifier"]) {
return response
.code(400)
.send({ error: "Missing cookie auth-token-code-verifier " });
}

const supabase = createClient({
request,
response,
});
const code = (
request.query as {
code: string;
}
).code;
if (!code) response.code(400).send({ error: "Missing code" });

const { data } = await supabase.auth.exchangeCodeForSession(code);
if (!data.session)
return response.code(400).send({ error: "Missing session" });
const providerToken = data.session.provider_token;
const providerRefreshToken = data.session.provider_refresh_token;
const providerName = data.session.user.app_metadata.provider;

if (!providerToken || !providerRefreshToken)
return response
.code(400)
.send({ error: "Missing provider token from " + providerName });

// verify if user already have an user_profile (if new acc, create one)
let userProfileId = await getUserProfile(data.user.id);

if (!userProfileId) {
// New account
const { userProfileId: newUserProfileId, error } = await createAccount({
full_name: data.user.user_metadata.full_name,
account_id: data.user.id,
username: null,
});
if (error || !newUserProfileId) {
request.log.error("Impossible to create account: " + error);
return response.code(500).send({ error: error });
}
userProfileId = newUserProfileId;
}
const providerTokenEnd = new Date();
providerTokenEnd.setHours(providerTokenEnd.getHours() + 1);
const timestampZProviderTokenEnd = providerTokenEnd.toISOString();

const error = await upsertService({
access_token: providerToken,
refresh_token: providerRefreshToken,
expires_in: timestampZProviderTokenEnd,
user_profile_id: userProfileId,
service_id: StreamingService.Spotify,
});

if (error) {
request.log.error("Upsert impossible, ", error);
return response.code(500).send({ error: "Server error." });
}

const refresh_token = data.session.refresh_token;
const redirectUrl = decodeURIComponent(request.url).split("redirect_url=")[1];

// redirect user to the redirect url with the refresh token
response.redirect(
redirectUrl + "#refresh_token=" + encodeURIComponent(refresh_token)
);
}

const getUserProfile = async (userId: string): Promise<string | null> => {
const { data: userData } = await adminSupabase
.from("user_profile")
.select("*")
.eq("account_id", userId)
.single();
return userData?.user_profile_id ?? null;
};

const alreadyBoundService = async ({
service,
user_profile_id,
}: {
service: StreamingService;
user_profile_id: string;
}): Promise<{ alreadyBound: boolean; error: PostgrestError | null }> => {
const { data, error } = await adminSupabase
.from("bound_services")
.select("*")
.eq("user_profile_id", user_profile_id)
.eq("service_id", service);
return {
alreadyBound: data !== null && data.length > 0,
error: error,
};
};

const createAccount = async ({
full_name,
account_id,
username,
}: {
full_name: string;
account_id: string;
username: string | null;
}): Promise<{ userProfileId: string | null; error: PostgrestError | null }> => {
const { data, error } = await adminSupabase
.from("profile")
.insert({
nickname: full_name,
})
.select("*")
.single();

const user_profile_id = data?.id;

if (!user_profile_id)
return {
userProfileId: null,
error: error,
};

const { data: dataUserProfile, error: errorUserprofile } = await adminSupabase
.from("user_profile")
.insert({
account_id: account_id,
user_profile_id: user_profile_id,
username: username, // Add the missing 'username' property
});
if (errorUserprofile)
return {
userProfileId: null,
error: errorUserprofile,
};
return {
userProfileId: user_profile_id,
error: null,
};
};

const upsertService = async (
service: Database["public"]["Tables"]["bound_services"]["Row"]
): Promise<PostgrestError | null> => {
const { error } = await adminSupabase.from("bound_services").upsert(service);
return error;
};
26 changes: 26 additions & 0 deletions backend/src/route/AuthRedirectionGET.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FastifyRequest, FastifyReply } from "fastify";

export default function AuthRedirectionGET(
req: FastifyRequest,
reply: FastifyReply
) {
reply.header("Content-Type", "text/html");
reply.code(200).send(ReponseHTML);
}

const ReponseHTML = `
<html>
<script>
const url = window.location.href;
const code_verifier = url.split("#")[1].split("=")[1];
const redirect_url = url.split("redirect_url=")[1].split("#")[0];
console.log("url", url);
console.log("code_verifier", code_verifier);
console.log("redirect url", redirect_url);
document.cookie = "sb-ckalsdcwrofxvgxslwiv-auth-token-code-verifier=" + code_verifier + "; path=/";
// redirect to the redirect url
window.location.href = redirect_url;
</script>
</html>`;
8 changes: 6 additions & 2 deletions backend/src/route/RoomGET.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { supabase } from "../server";
import createClient from "../lib/supabase";

interface QueryParams {
export interface QueryParams {
id: string;
}

Expand All @@ -10,6 +10,10 @@ export default function RoomGET(req: FastifyRequest, reply: FastifyReply) {
if (!id) {
reply.code(400).send({ error: "Missing id" });
}
const supabase = createClient({
request: req,
response: reply,
});
supabase
.from("rooms")
.select("*")
Expand Down
34 changes: 24 additions & 10 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { config } from "dotenv";
import fastify from "fastify";
import fastifyIO from "fastify-socket.io";
import path from "path";
import { Server } from "socket.io";
import HelloGet from "./route/HelloGET";
import RoomsGET from "./route/RoomGET";
import AuthCallbackGET from "./route/AuthCallbackGET";
import AuthRedirectionGET from "./route/AuthRedirectionGET";
import type { FastifyCookieOptions } from "@fastify/cookie";
import fastifyCors from "@fastify/cors";
import { createClient } from "@supabase/supabase-js";
import { config } from "dotenv";
import path from "path";
import { Database } from "./types/dbTypes";

config({ path: path.resolve(__dirname, "../.env.local") });

Expand All @@ -18,19 +21,30 @@ const server = fastify({
});

if (!process.env.SUPABASE_URL || !process.env.SERVICE_ROLE) {
throw new Error("Missing SUPABASE_URL or SUPABASE_KEY environment variable");
throw new Error(
"Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE environment variable"
);
}

export const supabase = createClient(
export const adminSupabase = createClient<Database>(
process.env.SUPABASE_URL,
process.env.SERVICE_ROLE
);

server.register(fastifyIO);
server.register(require("@fastify/cookie"), {
secret: process.env.FASTIFY_COOKIE_SECRET ?? "", // for cookies signature
hook: "onRequest", // set to false to disable cookie autoparsing or set autoparsing on any of the following hooks: 'onRequest', 'preParsing', 'preHandler', 'preValidation'. default: 'onRequest'
parseOptions: {}, // options for parsing cookies
} as FastifyCookieOptions);

server.get("/rooms", RoomsGET);
server.register(fastifyCors, {
origin: [true], // or true to allow all origins
methods: ["*"], // or just ['*'] for all methods
});

server.get("/hello", HelloGet);
// Auth
server.get("/auth/callback", AuthCallbackGET);
server.get("/auth/redirection", AuthRedirectionGET);

server.ready().then(() => {
// we need to wait for the server to be ready, else `server.io` is undefined
Expand All @@ -40,7 +54,7 @@ server.ready().then(() => {
});
});

server.listen({ port: 3000 });
server.listen({ port: 3000, host: "0.0.0.0" });

declare module "fastify" {
interface FastifyInstance {
Expand Down
Binary file added backend/src/types/dbTypes.ts
Binary file not shown.
Binary file added commons/Database-types.ts
Binary file not shown.
3 changes: 3 additions & 0 deletions expo/.env.exemple
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
EXPO_PUBLIC_SUPABASE_URL=
EXPO_PUBLIC_SUPABASE_ANON_KEY=

3 changes: 2 additions & 1 deletion expo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ web-build/
# The following patterns were generated by expo-cli

expo-env.d.ts
# @end expo-cli
# @end expo-cli
.env
2 changes: 1 addition & 1 deletion expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"scheme": "datsmysong",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
Expand Down
14 changes: 14 additions & 0 deletions expo/app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Stack } from "expo-router";

export default function TabLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen
name="login"
options={{ presentation: "modal", title: "Connexion" }}
/>
<Stack.Screen name="ask-name" options={{ headerShown: false }} />
</Stack>
);
}
Loading

0 comments on commit 7b655ba

Please sign in to comment.