🔧 use .env instead of json config files

This commit is contained in:
ChatonDeAru 2024-03-24 00:01:09 +01:00 committed by ChatonDeAru (Romain)
parent f88dbc36d2
commit 24db804c7f
16 changed files with 121 additions and 166 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
## Google Cloud Platform Service Account
GCP_SERVICE_ACCOUNT_PRIVATE_KEY=
GCP_SERVICE_ACCOUNT_CLIENT_ID=
GCP_SERVICE_ACCOUNT_CLIENT_EMAIL=
GSHEET_ID=
## Discord
DISCORD_TOKEN=
DISCORD_CLIENTID=
DISCORD_GUILDID=
## Notifications
FORCE_ORANGE_PUBLIC_VAPID_KEY=
FORCE_ORANGE_PRIVATE_VAPID_KEY=
## Environment
PORT=8080
API_URL=http://fo.parisestludique.fr/api

View File

@ -60,5 +60,6 @@ module.exports = {
__LOCAL__: false,
__REGISTER_DISCORD_COMMANDS__: false,
__TEST__: false,
API_URL: false,
},
}

View File

@ -24,6 +24,7 @@ module.exports = {
__LOCAL__: false,
__REGISTER_DISCORD_COMMANDS__: false,
__TEST__: true,
API_URL: "http://localhost:3000",
},
maxConcurrency: 50,
maxWorkers: 1,

View File

@ -39,12 +39,12 @@
"npm": ">=6"
},
"scripts": {
"dev": "yarn dev:build && nodemon ./public/server",
"ser": "yarn dev:build && node ./public/server",
"dev": "yarn dev:build && nodemon -r dotenv/config ./public/server",
"ser": "yarn dev:build && node -r dotenv/config ./public/server",
"dev:build": "cross-env NODE_ENV=development webpack --config ./webpack/server.config.ts",
"local-start": "cross-env LOCAL=true yarn build && node ./public/server",
"discord-register": "cross-env REGISTER_DISCORD_COMMANDS=true yarn build && node ./public/server",
"start": "node ./public/server",
"local-start": "cross-env LOCAL=true yarn build && node -r dotenv/config ./public/server",
"discord-register": "cross-env REGISTER_DISCORD_COMMANDS=true yarn build && node -r dotenv/config ./public/server",
"start": "node -r dotenv/config ./public/server",
"build": "run-s build:*",
"build:server": "cross-env NODE_ENV=production webpack --config ./webpack/server.config.ts",
"build:client": "cross-env NODE_ENV=production webpack --config ./webpack/client.config.ts",
@ -174,6 +174,7 @@
"compression-webpack-plugin": "^8.0.1",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv": "^16.4.5",
"eslint": "^7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",

View File

@ -2,7 +2,6 @@ import { RouteConfig, renderRoutes } from "react-router-config"
import { Helmet } from "react-helmet"
import { ToastContainer } from "react-toastify"
import config from "../config"
// Import your global styles here
import "normalize.css/normalize.css"
import "react-toastify/dist/ReactToastify.css"
@ -24,9 +23,22 @@ const App = ({ route, location }: Route): JSX.Element => {
}
// else
const appInfo = {
htmlAttributes: { lang: "en" },
title: "Force Orange",
description: "Le site des bénévoles",
titleTemplate: "Force Orange - %s",
meta: [
{
name: "description",
content: "The best react universal starter boilerplate in the world.",
},
],
}
return (
<div>
<Helmet {...config.APP}>
<Helmet {...appInfo}>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
@ -36,9 +48,9 @@ const App = ({ route, location }: Route): JSX.Element => {
<div className={styles.logo} />
<div>
<h1 className={styles.siteName}>
<a href="/">{config.APP.title}</a>
<a href="/">{appInfo.title}</a>
</h1>
<div className={styles.siteDescription}>{config.APP.description}</div>
<div className={styles.siteDescription}>{appInfo.description}</div>
</div>
<div className={styles.menuWrapper}>
<MainMenu />

View File

@ -1,20 +0,0 @@
export default {
HOST: "localhost",
PORT: 3000,
API_URL: "http://localhost:3000",
GOOGLE_SHEET_ID: "1g-GB1NHLtUAQnQkbyo_5Lv6GzJ58DeOsUTrHlIVi01M",
APP: {
htmlAttributes: { lang: "en" },
title: "Force Orange",
description: "Le site des bénévoles",
titleTemplate: "Force Orange - %s",
meta: [
{
name: "description",
content: "The best react universal starter boilerplate in the world.",
},
],
},
DEV_JWT_SECRET: "fakeqA6uF#msq2312bebf2FLFn4XzWQ6dttXSJwBX#?gL2JWf!",
DEV_DISCORD_TOKEN: "fakeqA6uF#msq2312bebf2FLFn4XzWQ6dttXSJwBX#?gL2JWf!",
}

View File

@ -1,4 +0,0 @@
import defaultConfig from "./default"
import prodConfig from "./prod"
export default __DEV__ ? defaultConfig : { ...defaultConfig, ...prodConfig }

View File

@ -1,14 +0,0 @@
import isNode from "detect-node"
const PROTOCOL = (typeof window !== "undefined" && window?.location?.protocol) || "http:"
const PORT = 4000 + (PROTOCOL === "https:" ? 2 : 0)
const API_URL =
__DEV__ || __LOCAL__ || isNode
? `${PROTOCOL}//localhost:${PORT}`
: `${PROTOCOL}//fo.parisestludique.fr`
export default {
PORT,
HOST: "0.0.0.0",
API_URL,
}

View File

@ -13,8 +13,6 @@ import {
User,
PartialUser,
} from "discord.js"
import { promises as fs, constants } from "fs"
import path from "path"
import { translationVolunteer, Volunteer, VolunteerWithoutId } from "../services/volunteers"
import {
@ -23,13 +21,8 @@ import {
DiscordRoleWithoutId,
} from "../services/discordRoles"
import { getSheet } from "./gsheets/accessors"
import config from "../config"
let cachedToken: string
// let cachedClientId: string
let cachedGuildId: string
const CREDS_PATH = path.resolve(process.cwd(), "access/discordToken.json")
getCreds() // Necessary until we can make async express middleware
const { DISCORD_TOKEN, DISCORD_CLIENTID, DISCORD_GUILDID } = process.env
type Command = {
data: SlashCommandBuilder
@ -52,19 +45,8 @@ const commands: Collection<string, Command> = new Collection()
// },
// }
let hasDiscordAccessReturn: boolean | undefined
export async function hasDiscordAccess(): Promise<boolean> {
if (hasDiscordAccessReturn !== undefined) {
return hasDiscordAccessReturn
}
try {
// eslint-disable-next-line no-bitwise
await fs.access(CREDS_PATH, constants.R_OK | constants.W_OK)
hasDiscordAccessReturn = true
} catch {
hasDiscordAccessReturn = false
}
return hasDiscordAccessReturn
export function hasDiscordAccess(): boolean {
return Boolean(DISCORD_TOKEN && DISCORD_CLIENTID && DISCORD_GUILDID)
}
// export async function discordRegisterCommands(): Promise<void> {
@ -72,7 +54,7 @@ export async function hasDiscordAccess(): Promise<boolean> {
// return
// }
// if (!(await hasDiscordAccess())) {
// if (!(hasDiscordAccess())) {
// console.error(`Discord bot: no creds found, not running bot`)
// return
// }
@ -82,11 +64,11 @@ export async function hasDiscordAccess(): Promise<boolean> {
// const commandsToRegister = []
// commandsToRegister.push(userCommand.data.toJSON())
// const rest = new REST({ version: '10' }).setToken(cachedToken)
// const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN)
// try {
// await rest.put(
// Routes.applicationGuildCommands(cachedClientId, cachedGuildId),
// Routes.applicationGuildCommands(DISCORD_CLIENTID, DISCORD_GUILDID),
// { body: commandsToRegister },
// )
// } catch (error) {
@ -102,11 +84,10 @@ export async function discordBot(): Promise<void> {
return
}
if (!(await hasDiscordAccess())) {
if (!hasDiscordAccess()) {
console.error(`Discord bot: no creds found, not running bot`)
return
}
await getCreds()
const client = new Client({
intents: [
@ -158,7 +139,7 @@ export async function discordBot(): Promise<void> {
await setRolesFromEmoji(client, user, reaction, "remove")
})
client.login(cachedToken)
client.login(DISCORD_TOKEN)
} catch (error) {
console.error("Discord error", error)
}
@ -205,6 +186,8 @@ async function setBotReactions(client: Client) {
async function setAllRoles(client: Client) {
try {
if (!DISCORD_GUILDID) return
const volunteerSheet = await getSheet<VolunteerWithoutId, Volunteer>(
"Volunteers",
new Volunteer(),
@ -216,8 +199,7 @@ async function setAllRoles(client: Client) {
}
const volunteerByDiscordId = _.mapKeys(volunteerList, (v) => v.discordId.toString())
const guild = await client.guilds.fetch(cachedGuildId)
const guild = await client.guilds.fetch(DISCORD_GUILDID)
if (!guild || !guild.members.cache) {
return
@ -356,6 +338,8 @@ async function setRolesFromEmoji(
reaction: MessageReaction | PartialMessageReaction,
action: "add" | "remove"
) {
if (!DISCORD_GUILDID) return
const discordRolesSheet = await getSheet<DiscordRoleWithoutId, DiscordRole>(
"DiscordRoles",
new DiscordRole(),
@ -367,7 +351,7 @@ async function setRolesFromEmoji(
}
await client.guilds.fetch()
const guild = client.guilds.resolve(cachedGuildId)
const guild = client.guilds.resolve(DISCORD_GUILDID)
if (!guild || !guild.members.cache) {
return
@ -408,20 +392,3 @@ async function fetchPartial(reaction: MessageReaction | PartialMessageReaction):
}
return true
}
async function getCreds(): Promise<void> {
if (!cachedToken) {
try {
const credsContent = await fs.readFile(CREDS_PATH)
const parsedCreds = credsContent && JSON.parse(credsContent.toString())
if (!parsedCreds) {
return
}
cachedToken = parsedCreds.token
// cachedClientId = parsedCreds.clientId
cachedGuildId = parsedCreds.guildId
} catch (e: any) {
cachedToken = config.DEV_DISCORD_TOKEN
}
}
}

View File

@ -1,7 +1,5 @@
// eslint-disable-next-line max-classes-per-file
import path from "path"
import _, { assign, pick } from "lodash"
import { promises as fs, constants } from "fs"
import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadsheet"
import { SheetNames, saveLocalDb, loadLocalDb } from "./localDb"
@ -9,14 +7,10 @@ export { SheetNames } from "./localDb"
// Test write attack with: wget --header='Content-Type:application/json' --post-data='{"prenom":"Pierre","nom":"SCELLES","email":"test@gmail.com","telephone":"0601010101","dejaBenevole":false,"commentaire":""}' http://localhost:3000/PostulantAdd
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
const REMOTE_UPDATE_DELAY = 120000
const DELAY_BETWEEN_ATTEMPTS = 30000
const DELAY_BETWEEN_FIRST_LOAD = 1500
let creds: string | undefined | null
export type ElementWithId<ElementNoId> = { id: number } & ElementNoId
export const sheetNames = new SheetNames()
@ -25,23 +19,16 @@ export const sheetNames = new SheetNames()
type SheetList = { [sheetName in keyof SheetNames]?: Sheet<object, ElementWithId<object>> }
const sheetList: SheetList = {}
let hasGSheetsAccessReturn: boolean | undefined
export async function hasGSheetsAccess(): Promise<boolean> {
if (hasGSheetsAccessReturn !== undefined) {
return hasGSheetsAccessReturn
}
try {
// eslint-disable-next-line no-bitwise
await fs.access(CRED_PATH, constants.R_OK | constants.W_OK)
hasGSheetsAccessReturn = true
} catch {
hasGSheetsAccessReturn = false
}
return hasGSheetsAccessReturn
export function hasGSheetsAccess(): boolean {
return Boolean(
process.env.GSHEET_ID &&
process.env.GCP_SERVICE_ACCOUNT_CLIENT_EMAIL &&
process.env.GCP_SERVICE_ACCOUNT_PRIVATE_KEY
)
}
export async function checkGSheetsAccess(): Promise<void> {
if (!(await hasGSheetsAccess())) {
if (!hasGSheetsAccess()) {
console.error(`Google Sheets: no creds found, loading local database instead`)
}
}
@ -367,22 +354,17 @@ export class Sheet<
private async getGSheet(attempts = 3): Promise<GoogleSpreadsheetWorksheet | null> {
return tryNTimes(
async () => {
if (creds === undefined) {
if (await hasGSheetsAccess()) {
const credsBuffer = await fs.readFile(CRED_PATH)
creds = credsBuffer?.toString() || null
} else {
creds = null
}
}
if (creds === null) {
return null
}
if (hasGSheetsAccess()) {
// Authentication
const doc = new GoogleSpreadsheet("1g-GB1NHLtUAQnQkbyo_5Lv6GzJ58DeOsUTrHlIVi01M")
await doc.useServiceAccountAuth(JSON.parse(creds))
const doc = new GoogleSpreadsheet(process.env.GSHEET_ID)
await doc.useServiceAccountAuth({
client_email: process.env.GCP_SERVICE_ACCOUNT_CLIENT_EMAIL || "",
private_key: process.env.GCP_SERVICE_ACCOUNT_PRIVATE_KEY || "",
})
await doc.loadInfo()
return doc.sheetsByTitle[this.sheetName]
}
return null
},
() => null,
attempts,

View File

@ -49,7 +49,7 @@ import {
volunteerOnSiteInfo,
} from "./gsheets/volunteers"
import { wishListGet, wishAdd } from "./gsheets/wishes"
import config from "../config"
import { notificationsSubscribe, notificationMain } from "./notifications"
import { /* discordRegisterCommands, */ discordBot, hasDiscordAccess } from "./discordBot"
import checkAccess from "./checkAccess"
@ -202,7 +202,9 @@ if (validCertPath) {
* Listen on provided port, on all network interfaces.
*/
servers.forEach(({ protocol, server }) => {
server.listen(protocol === "http" ? config.PORT : <number>config.PORT + 2)
const port = Number(process.env.PORT) || 3000
server.listen(protocol === "http" ? port : port + 2)
server.on("error", onError)
server.on("listening", () => onListening(server))
})
@ -227,13 +229,11 @@ function onListening(server: any) {
addStatus("Server listening:", chalk.green(`${bind}`))
}
hasGSheetsAccess().then((hasApiAccess: boolean) => {
if (hasApiAccess) {
if (hasGSheetsAccess()) {
addStatus("Database:", chalk.green(`✅ online from Google Sheet`))
} else {
} else {
addStatus("Database:", chalk.blue(`🚧 offline, simulated from local db file`))
}
})
}
const hasSendGridApiAccess = !!process.env.SENDGRID_API_KEY
if (hasSendGridApiAccess) {
@ -249,13 +249,11 @@ if (hasPushNotifAccess) {
addStatus("Push notif:", chalk.blue(`🚧 offline, simulated`))
}
hasDiscordAccess().then((hasApiAccess: boolean) => {
if (hasApiAccess) {
if (hasDiscordAccess()) {
addStatus("Discord bot:", chalk.green(`✅ online through discord.js`))
} else {
} else {
addStatus("Discord bot:", chalk.blue(`🚧 no creds, disabled`))
}
})
}
hasSecret().then((has: boolean) => {
if (has) {

View File

@ -3,8 +3,6 @@ import path from "path"
import { constants, promises as fs } from "fs"
import { verify, sign } from "jsonwebtoken"
import config from "../config"
type AuthorizedRequest = Request & { headers: { authorization: string } }
let cachedSecret: string
@ -57,7 +55,7 @@ async function getSecret() {
const secretContent = await fs.readFile(SECRET_PATH)
cachedSecret = secretContent && JSON.parse(secretContent.toString()).secret
} catch (e: any) {
cachedSecret = config.DEV_JWT_SECRET
cachedSecret = "fakeqA6uF#msq2312bebf2FLFn4XzWQ6dttXSJwBX#?gL2JWf!"
}
}

View File

@ -1,7 +1,6 @@
import axios from "axios"
import _ from "lodash"
import config from "../config"
import { axiosConfig } from "./auth"
export type ElementWithId = unknown & { id: number }
@ -26,7 +25,7 @@ export default class ServiceAccessors<
}
return async (id: number): Promise<ElementGetResponse> => {
try {
const { data } = await axios.get(`${config.API_URL}/${this.elementName}Get`, {
const { data } = await axios.get(`${API_URL}/${this.elementName}Get`, {
...axiosConfig,
params: { id },
})
@ -51,7 +50,7 @@ export default class ServiceAccessors<
return async (): Promise<ElementListGetResponse> => {
try {
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}ListGet`,
`${API_URL}/${this.elementName}ListGet`,
axiosConfig
)
if (data.error) {
@ -77,7 +76,7 @@ export default class ServiceAccessors<
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}ListGet`,
`${API_URL}/${this.elementName}ListGet`,
fullAxiosConfig
)
if (data.error) {
@ -107,7 +106,7 @@ export default class ServiceAccessors<
try {
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const rawData = await axios.get(`${config.API_URL}/${this.elementName}${apiName}`, {
const rawData = await axios.get(`${API_URL}/${this.elementName}${apiName}`, {
...fullAxiosConfig,
params,
})
@ -135,7 +134,7 @@ export default class ServiceAccessors<
return async (volunteerWithoutId: ElementNoId): Promise<ElementGetResponse> => {
try {
const { data } = await axios.post(
`${config.API_URL}/${this.elementName}Add`,
`${API_URL}/${this.elementName}Add`,
volunteerWithoutId,
axiosConfig
)
@ -160,7 +159,7 @@ export default class ServiceAccessors<
return async (volunteer: Element): Promise<ElementGetResponse> => {
try {
const { data } = await axios.post(
`${config.API_URL}/${this.elementName}Set`,
`${API_URL}/${this.elementName}Set`,
volunteer,
axiosConfig
)
@ -185,7 +184,7 @@ export default class ServiceAccessors<
return async (): Promise<ElementCountGetResponse> => {
try {
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}CountGet`,
`${API_URL}/${this.elementName}CountGet`,
axiosConfig
)
if (data.error) {
@ -210,10 +209,10 @@ export default class ServiceAccessors<
}
return async (...params: InputElements): Promise<ElementGetResponse> => {
try {
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}${apiName}`,
{ ...axiosConfig, params }
)
const { data } = await axios.get(`${API_URL}/${this.elementName}${apiName}`, {
...axiosConfig,
params,
})
if (data.error) {
throw Error(data.error)
}
@ -237,7 +236,7 @@ export default class ServiceAccessors<
return async (...params: InputElements): Promise<ElementGetResponse> => {
try {
const { data } = await axios.post(
`${config.API_URL}/${this.elementName}${apiName}`,
`${API_URL}/${this.elementName}${apiName}`,
params,
axiosConfig
)
@ -268,10 +267,10 @@ export default class ServiceAccessors<
try {
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}${apiName}`,
{ ...fullAxiosConfig, params }
)
const { data } = await axios.get(`${API_URL}/${this.elementName}${apiName}`, {
...fullAxiosConfig,
params,
})
if (data.error) {
throw Error(data.error)
}
@ -300,7 +299,7 @@ export default class ServiceAccessors<
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.post(
`${config.API_URL}/${this.elementName}${apiName}`,
`${API_URL}/${this.elementName}${apiName}`,
params,
fullAxiosConfig
)

View File

@ -4,6 +4,7 @@ declare const __DEV__: boolean
declare const __LOCAL__: boolean
declare const __REGISTER_DISCORD_COMMANDS__: boolean
declare const __TEST__: boolean
declare const API_URL: string
declare module "*.svg"
declare module "*.gif"
@ -22,6 +23,7 @@ declare namespace NodeJS {
__LOCAL__: boolean
__REGISTER_DISCORD_COMMANDS__: boolean
__TEST__: boolean
API_URL: string
$RefreshReg$: () => void
$RefreshSig$$: () => void
}
@ -30,6 +32,13 @@ declare namespace NodeJS {
SENDGRID_API_KEY?: string
FORCE_ORANGE_PUBLIC_VAPID_KEY?: string
FORCE_ORANGE_PRIVATE_VAPID_KEY?: string
PORT?: string
HOST?: string
API_URL?: string
GCP_SERVICE_ACCOUNT_CLIENT_ID?: string
GCP_SERVICE_ACCOUNT_PRIVATE_KEY?: string
GCP_SERVICE_ACCOUNT_CLIENT_EMAIL?: string
GSHEET_ID?: string
}
}

View File

@ -10,6 +10,7 @@ import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"
export const isDev = process.env.NODE_ENV === "development"
const isLocal = process.env.LOCAL === "true"
const isRegisterDiscordCommands = process.env.REGISTER_DISCORD_COMMANDS === "true"
const getStyleLoaders = (isWeb: boolean, isSass?: boolean) => {
let loaders: RuleSetUseItem[] = [
{
@ -51,6 +52,7 @@ const getPlugins = (isWeb: boolean) => {
__DEV__: isDev,
__LOCAL__: isLocal,
__REGISTER_DISCORD_COMMANDS__: isRegisterDiscordCommands,
API_URL: process.env.API_URL || "'http://localhost:3000'",
}),
]

View File

@ -4186,6 +4186,11 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
dotenv@^16.4.5:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
download@^6.2.2:
version "6.2.5"
resolved "https://registry.yarnpkg.com/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714"