」 feat: added google auth

This commit is contained in:
2025-07-18 17:28:47 +02:00
parent 1fb6d07d10
commit ff8a4863ba
10 changed files with 381 additions and 27 deletions

View File

@ -3,7 +3,10 @@ import fastifyCookie from '@fastify/cookie';
import { register } from './register.js';
import { login } from './login.js';
import { gRedir } from './gRedir.js';
import authDB from '../../utils/authDB.js'
import { gLogCallback } from './gLogCallback.js';
import { gRegisterCallback } from './gRegisterCallback.js';
const saltRounds = 10;
@ -36,7 +39,18 @@ export default async function(fastify, options) {
});
// GOOGLE sign in
fastify.get('/login/google', async (request, reply) => {
return gRedir(request, reply, fastify, '/login/google/callback');
});
fastify.get('/register/google', async (request, reply) => {
return gRedir(request, reply, fastify, '/register/google/callback');
});
fastify.get('/login/google/callback', async (request, reply) => {
return gLogCallback(request, reply, fastify);
})
fastify.get('/register/google/callback', async (request, reply) => {
return gRegisterCallback(request, reply, fastify);
})
fastify.post('/login', {
schema: {

View File

@ -0,0 +1,55 @@
import axios from 'axios'
import authDB from '../../utils/authDB.js';
var env = process.env.NODE_ENV || 'development';
/**
* @param {import("fastify").FastifyRequest} request
* @param {import("fastify").FastifyReply} reply
* @param {import("fastify").FastifyInstance} fastify
*
* @returns {import('fastify').FastifyReply}
*/
export async function gLogCallback(request, reply, fastify) {
const { code } = request.query;
try {
const response = await axios.post('https://oauth2.googleapis.com/token', {
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_CALLBACK_URL + '/login/google/callback',
grant_type: 'authorization_code',
});
const { access_token } = response.data;
const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: { Authorization: `Bearer ${access_token}` },
});
const userProfile = userInfoResponse.data;
const user = {
username: userProfile.email, // Assuming email is used as the username
};
if (!authDB.checkUser(user.username) || authDB.RESERVED_USERNAMES.includes(user.username)) {
return reply.code(400).send({ error: "User does not exist" });
}
const token = fastify.jwt.sign(user);
return reply
.setCookie('token', token, {
httpOnly: true,
path: '/',
secure: env !== 'development',
sameSite: 'lax',
})
.code(200)
.send({ msg: "Login successful" });
} catch (error) {
fastify.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
}
}

23
src/api/auth/gRedir.js Normal file
View File

@ -0,0 +1,23 @@
/**
* @param {import("fastify").FastifyRequest} request
* @param {import("fastify").FastifyReply} reply
* @param {import("fastify").FastifyInstance} fastify
* @param {string} callbackRoute
*
* @returns {import('fastify').FastifyReply}
*/
export async function gRedir(request, reply, fastify, callbackRoute) {
try {
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${process.env.GOOGLE_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(process.env.GOOGLE_CALLBACK_URL + callbackRoute)}&` +
`response_type=code&` +
`scope=email profile&` +
`access_type=offline`;
return reply.redirect(authUrl);
} catch (error) {
fastify.log.error(error);
return reply.code(500).send({ error: "Internal server error" });
}
}

View File

@ -0,0 +1,64 @@
import axios from 'axios'
import authDB from '../../utils/authDB.js';
var env = process.env.NODE_ENV || 'development';
/**
* @param {import("fastify").FastifyRequest} request
* @param {import("fastify").FastifyReply} reply
* @param {import("fastify").FastifyInstance} fastify
*
* @returns {import('fastify').FastifyReply}
*/
export async function gRegisterCallback(request, reply, fastify) {
const { code } = request.query;
try {
const response = await axios.post('https://oauth2.googleapis.com/token', {
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_CALLBACK_URL + '/register/google/callback',
grant_type: 'authorization_code',
}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const { access_token } = response.data;
const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: { Authorization: `Bearer ${access_token}` },
});
const userProfile = userInfoResponse.data;
const user = {
username: userProfile.email, // Assuming email is used as the username
};
if (authDB.RESERVED_USERNAMES.includes(user)) {
return reply.code(400).send({ error: 'Reserved username' });
}
if (authDB.checkUser(user.username) === true) {
return reply.code(400).send({ error: "User already exist" });
}
authDB.addUser(user.username, '');
const token = fastify.jwt.sign(user);
return reply
.setCookie('token', token, {
httpOnly: true,
path: '/',
secure: env !== 'development',
sameSite: 'lax',
})
.code(200)
.send({ msg: "Register successful" });
} catch (error) {
fastify.log.error(error);
reply.code(500).send({ error: 'Internal server error' });
}
}

View File

@ -1,6 +1,5 @@
import bcrypt from 'bcrypt';
import { checkUser } from '../../utils/authUtils.js';
import authDB from '../../utils/authDB.js';
var env = process.env.NODE_ENV || 'development';
@ -18,11 +17,11 @@ export async function login(request, reply, fastify) {
/** @type {{ user: string, password: string }} */
const { user, password } = request.body;
if (!checkUser(user) || user === 'admin') {
if (!authDB.checkUser(user) || authDB.RESERVED_USERNAMES.includes(user)) {
return reply.code(400).send({ error: "User does not exist" });
}
const query = authDB.passwordQuery.get(user);
const query = authDB.passwordQuery(user);
const hash = query?.passwordHash;
if (!hash) {

View File

@ -1,6 +1,6 @@
import bcrypt from 'bcrypt';
import { isValidString, checkUser } from '../../utils/authUtils.js';
import { isValidString } from '../../utils/authUtils.js';
import authDB from '../../utils/authDB.js';
var env = process.env.NODE_ENV || 'development';
@ -25,7 +25,7 @@ export async function register(request, reply, saltRounds, fastify) {
if (!isValidString(user) || !isValidString(password)) {
return reply.code(400).send({ error: 'Invalid username or password' });
} else if (checkUser(user) === true) {
} else if (authDB.checkUser(user) === true) {
return reply.code(400).send({ error: "User already exist" });
} else if (password.length <= 8) {
return reply.code(400).send({ error: "Password too short" });
@ -34,7 +34,7 @@ export async function register(request, reply, saltRounds, fastify) {
}
const hash = await bcrypt.hash(password, saltRounds);
authDB.userAdd.run(user, hash);
authDB.addUser(user, hash);
const token = fastify.jwt.sign({ user });

View File

@ -3,7 +3,6 @@ import Database from 'better-sqlite3';
var env = process.env.NODE_ENV || 'development';
let database;
const RESERVED_USERNAMES = ['admin'];
let userCheck, passwordQuery, userAdd;
if (!env || env === 'development') {
database = new Database(":memory:", { verbose: console.log });
@ -22,17 +21,39 @@ function prepareDB() {
passwordHash TEXT
) STRICT
`);
userCheck = database.prepare('SELECT EXISTS (SELECT 1 FROM credentials WHERE username = ?);');
passwordQuery = database.prepare('SELECT passwordHash FROM credentials WHERE username = ?;');
userAdd = database.prepare('INSERT INTO credentials (username, passwordHash) VALUES (?, ?)');
}
/**
* @param {string} name
*
* @returns {boolean}
*/
function checkUser(name) {
/**
* @type: {import('better-sqlite3').Statement}
*/
let userCheck = database.prepare('SELECT EXISTS (SELECT 1 FROM credentials WHERE username = ?);');
const result = userCheck.get(name);
const key = Object.keys(result)[0];
return result[key] === 1;
}
function addUser(name, pass) {
let userAdd = database.prepare('INSERT INTO credentials (username, passwordHash) VALUES (?, ?)');
userAdd.run(name, pass);
}
function passwordQuery(user) {
let passwordQuery = database.prepare('SELECT passwordHash FROM credentials WHERE username = ?;');
return passwordQuery.get(user)
}
const authDB = {
prepareDB,
get userCheck() { return userCheck; },
get userAdd() { return userAdd; },
get passwordQuery() { return passwordQuery; },
checkUser,
addUser,
passwordQuery,
RESERVED_USERNAMES
};

View File

@ -8,16 +8,3 @@ import authDB from './authDB.js';
export function isValidString(value) {
return typeof value === 'string' && value.trim() !== '';
}
/**
* @param {string} name
* @param {import('better-sqlite3').Statement} userCheck
*
* @returns {boolean}
*/
export function checkUser(name, userCheck) {
const result = authDB.userCheck.get(name);
const key = Object.keys(result)[0];
return result[key] === 1;
}