Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/spotify-account #32

Merged
merged 19 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading