diff --git a/app/src/hooks.server.ts b/app/src/hooks.server.ts index 03f35d1..b01aa8c 100644 --- a/app/src/hooks.server.ts +++ b/app/src/hooks.server.ts @@ -1,4 +1,32 @@ import type { Handle } from '@sveltejs/kit'; import { i18n } from '$lib/i18n'; +import { validateSessionToken, setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/server/session"; +import { sequence } from "@sveltejs/kit/hooks"; + const handleParaglide: Handle = i18n.handle(); -export const handle: Handle = handleParaglide; + +const authHandle: Handle = async ({ event, resolve }) => { + const token = event.cookies.get("session") ?? null; + if (token === null) { + event.locals.session = null; + return resolve(event); + } + + if(!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS){ + return new Response("Server configuration error. GH_SESSIONS KeyValue store missing", { + status: 500 + }); + } + + const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS); + if (session !== null) { + setSessionTokenCookie(event, token, session.expiresAt); + } else { + deleteSessionTokenCookie(event); + } + + event.locals.session = session; + return resolve(event); +}; + +export const handle: Handle = sequence(handleParaglide, authHandle); \ No newline at end of file diff --git a/app/src/lib/server/db.ts b/app/src/lib/server/db.ts new file mode 100644 index 0000000..10ccce8 --- /dev/null +++ b/app/src/lib/server/db.ts @@ -0,0 +1,11 @@ +import memgraph from 'neo4j-driver'; +import type { Driver } from 'neo4j-driver'; +import { MEMEGRAPH_URI, MEMGRAPH_USER, MEMGRAPH_PASSWORD } from '$env/static/private'; + +export const driverInstance: Driver = memgraph.driver( + MEMEGRAPH_URI || 'bolt://localhost:7687', + memgraph.auth.basic( + MEMGRAPH_USER || 'memgraph', + MEMGRAPH_PASSWORD || 'memgraph' + ) +); diff --git a/app/src/lib/server/oauth.ts b/app/src/lib/server/oauth.ts new file mode 100644 index 0000000..1e4a11f --- /dev/null +++ b/app/src/lib/server/oauth.ts @@ -0,0 +1,4 @@ +import { Google } from "arctic"; +import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from "$env/static/private"; + +export const google = new Google(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI || "http://localhost:5173/login/google/callback"); \ No newline at end of file diff --git a/app/src/lib/server/user.ts b/app/src/lib/server/user.ts new file mode 100644 index 0000000..693b594 --- /dev/null +++ b/app/src/lib/server/user.ts @@ -0,0 +1,37 @@ +import type { Session } from 'neo4j-driver'; + +export function createUser(db: Session, googleId: string, email: string, first_name: string, family_name: string, middle_name: string): User { + const row = db.run + if (row === null) { + throw new Error("Unexpected error"); + } + const user: User = { + id: row.number(0), + googleId, + email, + }; + return user; +} + +export function getUserFromGoogleId(googleId: string): User | null { + const row = db.queryOne("SELECT id, google_id, email, name, picture FROM user WHERE google_id = ?", [googleId]); + if (row === null) { + return null; + } + const user: User = { + id: row.number(0), + googleId: row.string(1), + email: row.string(2), + name: row.string(3), + picture: row.string(4) + }; + return user; +} + +export interface User { + id: number; + email: string; + googleId: string; + name: string; + picture: string; +} diff --git a/app/src/routes/+page.server.ts b/app/src/routes/+page.server.ts new file mode 100644 index 0000000..935f5d1 --- /dev/null +++ b/app/src/routes/+page.server.ts @@ -0,0 +1,32 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { deleteSessionTokenCookie, invalidateSession } from "$lib/server/session"; +import type { Actions, RequestEvent } from "./$types"; + +export async function load(event: RequestEvent) { + if (event.locals.session === null /*|| event.locals.familytree === nul*/) { + return redirect(302, "/login"); + } + return { + // TODO - Add Family Graph + }; +} + +export const actions: Actions = { + logout: logout +}; + +async function logout(event: RequestEvent) { + if (event.locals.session === null) { + return fail(401); + } + + if (event.platform && event.platform.env && event.platform.env.GH_SESSIONS) { + invalidateSession(event.locals.session.id, event.platform.env.GH_SESSIONS); + } else { + return fail(500, { message: "Server configuration error" }); + } + + deleteSessionTokenCookie(event); + + return redirect(302, "/login"); +} \ No newline at end of file diff --git a/app/src/routes/login/+page.server.ts b/app/src/routes/login/+page.server.ts index 0c3f862..cbc7558 100644 --- a/app/src/routes/login/+page.server.ts +++ b/app/src/routes/login/+page.server.ts @@ -3,7 +3,7 @@ import { redirect } from "@sveltejs/kit"; import type { RequestEvent } from "./$types"; export async function load(event: RequestEvent) { - if (event.locals.session !== null && event.locals.user !== null) { + if (event.locals.session !== null) { return redirect(302, "/"); } return {}; diff --git a/app/src/routes/login/+page.svelte b/app/src/routes/login/+page.svelte new file mode 100644 index 0000000..1247599 --- /dev/null +++ b/app/src/routes/login/+page.svelte @@ -0,0 +1,23 @@ + + + + {title({ page: sign_in() })} + + +
+
+
+

{sign_in()}

+

+ Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem + quasi. In deleniti eaque aut repudiandae et a id nisi. +

+ + {sign_in()} + +
+
+
\ No newline at end of file diff --git a/app/src/routes/login/google/+server.ts b/app/src/routes/login/google/+server.ts new file mode 100644 index 0000000..8c440f7 --- /dev/null +++ b/app/src/routes/login/google/+server.ts @@ -0,0 +1,32 @@ +import { google } from "$lib/server/oauth"; +import { generateCodeVerifier, generateState } from "arctic"; + +import type { RequestEvent } from "./$types"; + +export function GET(event: RequestEvent): Response { + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile", "email"]); + + event.cookies.set("google_oauth_state", state, { + httpOnly: true, + maxAge: 60 * 10, + secure: import.meta.env.PROD, + path: "/", + sameSite: "lax" + }); + event.cookies.set("google_code_verifier", codeVerifier, { + httpOnly: true, + maxAge: 60 * 10, + secure: import.meta.env.PROD, + path: "/", + sameSite: "lax" + }); + + return new Response(null, { + status: 302, + headers: { + Location: url.toString() + } + }); +} \ No newline at end of file diff --git a/app/src/routes/login/google/callback/+server.ts b/app/src/routes/login/google/callback/+server.ts new file mode 100644 index 0000000..756a22f --- /dev/null +++ b/app/src/routes/login/google/callback/+server.ts @@ -0,0 +1,81 @@ +import { fail } from "@sveltejs/kit"; +import { google } from "$lib/server/oauth"; +import { ObjectParser } from "@pilcrowjs/object-parser"; +import { createUser, getUserFromGoogleId } from "$lib/server/user"; +import { driverInstance } from "$lib/server/db"; +import { createSession, generateSessionToken, setSessionTokenCookie } from "$lib/server/session"; +import { decodeIdToken } from "arctic"; + +import type { RequestEvent } from "./$types"; +import type { OAuth2Tokens } from "arctic"; +import { middle_name } from "$lib/paraglide/messages"; + +export async function GET(event: RequestEvent): Promise { + const storedState = event.cookies.get("google_oauth_state") ?? null; + const codeVerifier = event.cookies.get("google_code_verifier") ?? null; + const code = event.url.searchParams.get("code"); + const state = event.url.searchParams.get("state"); + + if (storedState === null || codeVerifier === null || code === null || state === null) { + return new Response("Please restart the process.", { + status: 400 + }); + } + if (storedState !== state) { + return new Response("Please restart the process.", { + status: 400 + }); + } + + let tokens: OAuth2Tokens; + try { + tokens = await google.validateAuthorizationCode(code, codeVerifier); + } catch (e) { + return new Response("Please restart the process.", { + status: 400 + }); + } + + if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) { + return new Response("Server configuration error. GH_SESSIONS KeyValue store missing", { + status: 500 + }); + } + + const claims = decodeIdToken(tokens.idToken()); + const claimsParser = new ObjectParser(claims); + + const googleId = claimsParser.getString("sub"); + const family_name = claimsParser.getString("family_name"); + const first_name = claimsParser.getString("given_name"); + const middle_name = claimsParser.getString("middle_name"); + const picture = claimsParser.getString("picture"); + const email = claimsParser.getString("email"); + + const existingUser = getUserFromGoogleId(googleId); + if (existingUser !== null) { + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, existingUser.id, event.platform.env.GH_SESSIONS); + setSessionTokenCookie(event, sessionToken, session.expiresAt); + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); + } + + const dbSession = driverInstance.session() + const user = createUser(dbSession, googleId, email, first_name, family_name, middle_name); + dbSession.close(); + const sessionToken = generateSessionToken(); + const session = await createSession(sessionToken, user.id, event.platform.env.GH_SESSIONS); + setSessionTokenCookie(event, sessionToken, session.expiresAt); + + return new Response(null, { + status: 302, + headers: { + Location: "/" + } + }); +} \ No newline at end of file diff --git a/app/src/routes/login/web_neutral_rd_SI.svg b/app/src/routes/login/web_neutral_rd_SI.svg new file mode 100644 index 0000000..74b3e5c --- /dev/null +++ b/app/src/routes/login/web_neutral_rd_SI.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +