Implement a more robust Config (#60)
* Implement a more robust Config This moves the configuration variables from the `.env` file to a new `bewcloud.config.ts` file. Note that DB connection and secrets are still in the `.env` file. This will allow for more reliable and easier personalized configurations, and was a requirement to start working on adding SSO (#13). For now, `.env`-based config will still be allowed and respected (overriden by `bewcloud.config.ts`), but in the future I'll probably remove it (some major upgrade). * Update deploy script to also copy the new config file
This commit is contained in:
27
lib/auth.ts
27
lib/auth.ts
@@ -3,10 +3,10 @@ import { decodeBase64 } from 'std/encoding/base64.ts';
|
||||
import { Cookie, getCookies, setCookie } from 'std/http/cookie.ts';
|
||||
import 'std/dotenv/load.ts';
|
||||
|
||||
import { baseUrl, generateHash, isRunningLocally } from './utils/misc.ts';
|
||||
import { generateHash, isRunningLocally } from './utils/misc.ts';
|
||||
import { User, UserSession } from './types.ts';
|
||||
import { UserModel, UserSessionModel, validateUserAndSession } from './models/user.ts';
|
||||
import { isCookieDomainAllowed, isCookieDomainSecurityDisabled } from './config.ts';
|
||||
import { AppConfig } from './config.ts';
|
||||
|
||||
const JWT_SECRET = Deno.env.get('JWT_SECRET') || '';
|
||||
export const PASSWORD_SALT = Deno.env.get('PASSWORD_SALT') || '';
|
||||
@@ -19,7 +19,7 @@ export interface JwtData {
|
||||
};
|
||||
}
|
||||
|
||||
const isBaseUrlAnIp = () => /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(baseUrl);
|
||||
const isUrlAnIp = (baseUrl: string) => /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(baseUrl);
|
||||
|
||||
const textToData = (text: string) => new TextEncoder().encode(text);
|
||||
|
||||
@@ -51,10 +51,13 @@ async function verifyAuthJwt(key: CryptoKey, jwt: string) {
|
||||
throw new Error('Invalid JWT');
|
||||
}
|
||||
|
||||
function resolveCookieDomain(request: Request) {
|
||||
if (!isBaseUrlAnIp() || isRunningLocally(request)) {
|
||||
async function resolveCookieDomain(request: Request) {
|
||||
const config = await AppConfig.getConfig();
|
||||
const baseUrl = config.auth.baseUrl;
|
||||
|
||||
if (!isUrlAnIp(baseUrl) || isRunningLocally(request)) {
|
||||
const domain = new URL(request.url).hostname;
|
||||
if (isCookieDomainAllowed(domain)) {
|
||||
if (await AppConfig.isCookieDomainAllowed(domain)) {
|
||||
return domain;
|
||||
}
|
||||
return baseUrl.replace('https://', '').replace('http://', '').split(':')[0];
|
||||
@@ -170,10 +173,10 @@ export async function logoutUser(request: Request) {
|
||||
secure: isRunningLocally(request) ? false : true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
domain: resolveCookieDomain(request),
|
||||
domain: await resolveCookieDomain(request),
|
||||
};
|
||||
|
||||
if (isCookieDomainSecurityDisabled()) {
|
||||
if (await AppConfig.isCookieDomainSecurityDisabled()) {
|
||||
delete cookie.domain;
|
||||
}
|
||||
|
||||
@@ -223,10 +226,10 @@ export async function createSessionCookie(
|
||||
secure: isRunningLocally(request) ? false : true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
domain: resolveCookieDomain(request),
|
||||
domain: await resolveCookieDomain(request),
|
||||
};
|
||||
|
||||
if (isCookieDomainSecurityDisabled()) {
|
||||
if (await AppConfig.isCookieDomainSecurityDisabled()) {
|
||||
delete cookie.domain;
|
||||
}
|
||||
|
||||
@@ -251,10 +254,10 @@ export async function updateSessionCookie(
|
||||
secure: isRunningLocally(request) ? false : true,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax',
|
||||
domain: resolveCookieDomain(request),
|
||||
domain: await resolveCookieDomain(request),
|
||||
};
|
||||
|
||||
if (isCookieDomainSecurityDisabled()) {
|
||||
if (await AppConfig.isCookieDomainSecurityDisabled()) {
|
||||
delete cookie.domain;
|
||||
}
|
||||
|
||||
|
||||
236
lib/config.ts
236
lib/config.ts
@@ -1,57 +1,203 @@
|
||||
import 'std/dotenv/load.ts';
|
||||
|
||||
import { UserModel } from './models/user.ts';
|
||||
import { Config, OptionalApp } from './types.ts';
|
||||
|
||||
export async function isSignupAllowed() {
|
||||
const areSignupsAllowed = Deno.env.get('CONFIG_ALLOW_SIGNUPS') === 'true';
|
||||
export class AppConfig {
|
||||
private static config: Config;
|
||||
|
||||
const areThereAdmins = await UserModel.isThereAnAdmin();
|
||||
|
||||
if (areSignupsAllowed || !areThereAdmins) {
|
||||
return true;
|
||||
private static getDefaultConfig(): Config {
|
||||
return {
|
||||
auth: {
|
||||
baseUrl: 'http://localhost:8000',
|
||||
allowSignups: false,
|
||||
enableEmailVerification: false,
|
||||
enableForeverSignup: true,
|
||||
allowedCookieDomains: [],
|
||||
skipCookieDomainSecurity: false,
|
||||
},
|
||||
files: {
|
||||
rootPath: 'data-files',
|
||||
},
|
||||
core: {
|
||||
enabledApps: ['news', 'notes', 'photos', 'expenses'],
|
||||
},
|
||||
visuals: {
|
||||
title: '',
|
||||
description: '',
|
||||
helpEmail: 'help@bewcloud.com',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
/** This allows for backwards-compatibility with the old config format, which was in the .env file. */
|
||||
private static async getLegacyConfigFromEnv(): Promise<Config> {
|
||||
const defaultConfig = this.getDefaultConfig();
|
||||
|
||||
export function isAppEnabled(app: 'news' | 'notes' | 'photos' | 'expenses') {
|
||||
const enabledApps = (Deno.env.get('CONFIG_ENABLED_APPS') || '').split(',') as typeof app[];
|
||||
if (typeof Deno === 'undefined') {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
return enabledApps.includes(app);
|
||||
}
|
||||
await import('std/dotenv/load.ts');
|
||||
|
||||
export function isCookieDomainAllowed(domain: string) {
|
||||
const allowedDomains = (Deno.env.get('CONFIG_ALLOWED_COOKIE_DOMAINS') || '').split(',') as typeof domain[];
|
||||
const baseUrl = Deno.env.get('BASE_URL') ?? defaultConfig.auth.baseUrl;
|
||||
const allowSignups = Deno.env.get('CONFIG_ALLOW_SIGNUPS') === 'true';
|
||||
const enabledApps = (Deno.env.get('CONFIG_ENABLED_APPS') ?? '').split(',') as OptionalApp[];
|
||||
const filesRootPath = Deno.env.get('CONFIG_FILES_ROOT_PATH') ?? defaultConfig.files.rootPath;
|
||||
const enableEmailVerification = (Deno.env.get('CONFIG_ENABLE_EMAILS') ?? 'false') === 'true';
|
||||
const enableForeverSignup = (Deno.env.get('CONFIG_ENABLE_FOREVER_SIGNUP') ?? 'true') === 'true';
|
||||
const allowedCookieDomains = (Deno.env.get('CONFIG_ALLOWED_COOKIE_DOMAINS') || '').split(',').filter(
|
||||
Boolean,
|
||||
) as string[];
|
||||
const skipCookieDomainSecurity = Deno.env.get('CONFIG_SKIP_COOKIE_DOMAIN_SECURITY') === 'true';
|
||||
const title = Deno.env.get('CUSTOM_TITLE') ?? defaultConfig.visuals.title;
|
||||
const description = Deno.env.get('CUSTOM_DESCRIPTION') ?? defaultConfig.visuals.description;
|
||||
const helpEmail = Deno.env.get('HELP_EMAIL') ?? defaultConfig.visuals.helpEmail;
|
||||
|
||||
if (allowedDomains.length === 0) {
|
||||
return true;
|
||||
return {
|
||||
...defaultConfig,
|
||||
auth: {
|
||||
...defaultConfig.auth,
|
||||
baseUrl,
|
||||
allowSignups,
|
||||
enableEmailVerification,
|
||||
enableForeverSignup,
|
||||
allowedCookieDomains,
|
||||
skipCookieDomainSecurity,
|
||||
},
|
||||
files: {
|
||||
...defaultConfig.files,
|
||||
rootPath: filesRootPath,
|
||||
},
|
||||
core: {
|
||||
...defaultConfig.core,
|
||||
enabledApps,
|
||||
},
|
||||
visuals: {
|
||||
...defaultConfig.visuals,
|
||||
title,
|
||||
description,
|
||||
helpEmail,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return allowedDomains.includes(domain);
|
||||
}
|
||||
|
||||
export function isCookieDomainSecurityDisabled() {
|
||||
const isCookieDomainSecurityDisabled = Deno.env.get('CONFIG_SKIP_COOKIE_DOMAIN_SECURITY') === 'true';
|
||||
|
||||
return isCookieDomainSecurityDisabled;
|
||||
}
|
||||
|
||||
export function isEmailEnabled() {
|
||||
const areEmailsAllowed = Deno.env.get('CONFIG_ENABLE_EMAILS') === 'true';
|
||||
|
||||
return areEmailsAllowed;
|
||||
}
|
||||
|
||||
export function isForeverSignupEnabled() {
|
||||
const areForeverAccountsEnabled = Deno.env.get('CONFIG_ENABLE_FOREVER_SIGNUP') === 'true';
|
||||
|
||||
return areForeverAccountsEnabled;
|
||||
}
|
||||
|
||||
export function getFilesRootPath() {
|
||||
const configRootPath = Deno.env.get('CONFIG_FILES_ROOT_PATH') || '';
|
||||
|
||||
const filesRootPath = `${Deno.cwd()}/${configRootPath}`;
|
||||
|
||||
return filesRootPath;
|
||||
private static async loadConfig(): Promise<void> {
|
||||
if (this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
let initialConfig = this.getDefaultConfig();
|
||||
|
||||
if (
|
||||
typeof Deno.env.get('BASE_URL') === 'string' || typeof Deno.env.get('CONFIG_ALLOW_SIGNUPS') === 'string' ||
|
||||
typeof Deno.env.get('CONFIG_ENABLED_APPS') === 'string'
|
||||
) {
|
||||
console.warn(
|
||||
'\nDEPRECATION WARNING: .env file has config variables. This will be used but is deprecated. Please use the bewcloud.config.ts file instead.',
|
||||
);
|
||||
|
||||
initialConfig = await this.getLegacyConfigFromEnv();
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
...initialConfig,
|
||||
};
|
||||
|
||||
try {
|
||||
const configFromFile: Config = (await import(`${Deno.cwd()}/bewcloud.config.ts`)).default;
|
||||
|
||||
this.config = {
|
||||
...config,
|
||||
auth: {
|
||||
...config.auth,
|
||||
...configFromFile.auth,
|
||||
},
|
||||
files: {
|
||||
...config.files,
|
||||
...configFromFile.files,
|
||||
},
|
||||
core: {
|
||||
...config.core,
|
||||
...configFromFile.core,
|
||||
},
|
||||
visuals: {
|
||||
...config.visuals,
|
||||
...configFromFile.visuals,
|
||||
},
|
||||
};
|
||||
|
||||
console.info('\nConfig loaded from bewcloud.config.ts', JSON.stringify(this.config, null, 2), '\n');
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Error loading config from bewcloud.config.ts. Using default and legacy config instead.', error);
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
static async getConfig(): Promise<Config> {
|
||||
await this.loadConfig();
|
||||
|
||||
return this.config;
|
||||
}
|
||||
|
||||
static async isSignupAllowed(): Promise<boolean> {
|
||||
await this.loadConfig();
|
||||
|
||||
const areSignupsAllowed = this.config.auth.allowSignups;
|
||||
|
||||
const areThereAdmins = await UserModel.isThereAnAdmin();
|
||||
|
||||
if (areSignupsAllowed || !areThereAdmins) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static async isAppEnabled(app: OptionalApp): Promise<boolean> {
|
||||
await this.loadConfig();
|
||||
|
||||
const enabledApps = this.config.core.enabledApps;
|
||||
|
||||
return enabledApps.includes(app);
|
||||
}
|
||||
|
||||
static async isCookieDomainAllowed(domain: string): Promise<boolean> {
|
||||
await this.loadConfig();
|
||||
|
||||
const allowedDomains = this.config.auth.allowedCookieDomains;
|
||||
|
||||
if (allowedDomains.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return allowedDomains.includes(domain);
|
||||
}
|
||||
|
||||
static async isCookieDomainSecurityDisabled(): Promise<boolean> {
|
||||
await this.loadConfig();
|
||||
|
||||
return this.config.auth.skipCookieDomainSecurity;
|
||||
}
|
||||
|
||||
static async isEmailVerificationEnabled(): Promise<boolean> {
|
||||
await this.loadConfig();
|
||||
|
||||
return this.config.auth.enableEmailVerification;
|
||||
}
|
||||
|
||||
static async isForeverSignupEnabled(): Promise<boolean> {
|
||||
await this.loadConfig();
|
||||
|
||||
return this.config.auth.enableForeverSignup;
|
||||
}
|
||||
|
||||
static async getFilesRootPath(): Promise<string> {
|
||||
await this.loadConfig();
|
||||
|
||||
const filesRootPath = `${Deno.cwd()}/${this.config.files.rootPath}`;
|
||||
|
||||
return filesRootPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { join } from 'std/path/join.ts';
|
||||
import { resolve } from 'std/path/resolve.ts';
|
||||
import { lookup } from 'mrmime';
|
||||
|
||||
import { getFilesRootPath } from '/lib/config.ts';
|
||||
import { AppConfig } from '/lib/config.ts';
|
||||
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||
import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
|
||||
|
||||
export class DirectoryModel {
|
||||
static async list(userId: string, path: string): Promise<Directory[]> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
const directories: Directory[] = [];
|
||||
|
||||
@@ -40,9 +40,9 @@ export class DirectoryModel {
|
||||
}
|
||||
|
||||
static async create(userId: string, path: string, name: string): Promise<boolean> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
try {
|
||||
await Deno.mkdir(join(rootPath, name), { recursive: true });
|
||||
@@ -72,7 +72,7 @@ export class DirectoryModel {
|
||||
userId: string,
|
||||
searchTerm: string,
|
||||
): Promise<{ success: boolean; directories: Directory[] }> {
|
||||
const rootPath = join(getFilesRootPath(), userId);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId);
|
||||
|
||||
const directories: Directory[] = [];
|
||||
|
||||
@@ -142,9 +142,9 @@ export class DirectoryModel {
|
||||
|
||||
export class FileModel {
|
||||
static async list(userId: string, path: string): Promise<DirectoryFile[]> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
const files: DirectoryFile[] = [];
|
||||
|
||||
@@ -177,9 +177,9 @@ export class FileModel {
|
||||
name: string,
|
||||
contents: string | ArrayBuffer,
|
||||
): Promise<boolean> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
try {
|
||||
// Ensure the directory exist, if being requested
|
||||
@@ -210,9 +210,9 @@ export class FileModel {
|
||||
name: string,
|
||||
contents: string,
|
||||
): Promise<boolean> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
try {
|
||||
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: false });
|
||||
@@ -229,9 +229,9 @@ export class FileModel {
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<{ success: boolean; contents?: Uint8Array; contentType?: string; byteSize?: number }> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name || ''));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name || ''));
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
try {
|
||||
const stat = await Deno.stat(join(rootPath, name || ''));
|
||||
@@ -277,7 +277,7 @@ export class FileModel {
|
||||
userId: string,
|
||||
searchTerm: string,
|
||||
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
||||
const rootPath = join(getFilesRootPath(), userId);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId);
|
||||
|
||||
const files: DirectoryFile[] = [];
|
||||
|
||||
@@ -348,7 +348,7 @@ export class FileModel {
|
||||
userId: string,
|
||||
searchTerm: string,
|
||||
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
||||
const rootPath = join(getFilesRootPath(), userId);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId);
|
||||
|
||||
const files: DirectoryFile[] = [];
|
||||
|
||||
@@ -421,8 +421,8 @@ export class FileModel {
|
||||
* @param userId - The user ID
|
||||
* @param path - The relative path (user-provided) to check
|
||||
*/
|
||||
export function ensureUserPathIsValidAndSecurelyAccessible(userId: string, path: string): void {
|
||||
const userRootPath = join(getFilesRootPath(), userId, '/');
|
||||
export async function ensureUserPathIsValidAndSecurelyAccessible(userId: string, path: string): Promise<void> {
|
||||
const userRootPath = join(await AppConfig.getFilesRootPath(), userId, '/');
|
||||
|
||||
const fullPath = join(userRootPath, path);
|
||||
|
||||
@@ -434,9 +434,9 @@ export function ensureUserPathIsValidAndSecurelyAccessible(userId: string, path:
|
||||
}
|
||||
|
||||
async function getPathEntries(userId: string, path: string): Promise<Deno.DirEntry[]> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, path);
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
// Ensure the user directory exists
|
||||
if (path === '/') {
|
||||
@@ -478,11 +478,11 @@ async function renameDirectoryOrFile(
|
||||
oldName: string,
|
||||
newName: string,
|
||||
): Promise<boolean> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(oldPath, oldName));
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(newPath, newName));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(oldPath, oldName));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(newPath, newName));
|
||||
|
||||
const oldRootPath = join(getFilesRootPath(), userId, oldPath);
|
||||
const newRootPath = join(getFilesRootPath(), userId, newPath);
|
||||
const oldRootPath = join(await AppConfig.getFilesRootPath(), userId, oldPath);
|
||||
const newRootPath = join(await AppConfig.getFilesRootPath(), userId, newPath);
|
||||
|
||||
try {
|
||||
await Deno.rename(join(oldRootPath, oldName), join(newRootPath, newName));
|
||||
@@ -495,15 +495,15 @@ async function renameDirectoryOrFile(
|
||||
}
|
||||
|
||||
async function deleteDirectoryOrFile(userId: string, path: string, name: string): Promise<boolean> {
|
||||
ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
await ensureUserPathIsValidAndSecurelyAccessible(userId, join(path, name));
|
||||
|
||||
const rootPath = join(getFilesRootPath(), userId, path);
|
||||
const rootPath = join(await AppConfig.getFilesRootPath(), userId, path);
|
||||
|
||||
try {
|
||||
if (path.startsWith(TRASH_PATH)) {
|
||||
await Deno.remove(join(rootPath, name), { recursive: true });
|
||||
} else {
|
||||
const trashPath = join(getFilesRootPath(), userId, TRASH_PATH);
|
||||
const trashPath = join(await AppConfig.getFilesRootPath(), userId, TRASH_PATH);
|
||||
await Deno.rename(join(rootPath, name), join(trashPath, name));
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
||||
import { User, UserSession, VerificationCode } from '/lib/types.ts';
|
||||
import { generateRandomCode } from '/lib/utils/misc.ts';
|
||||
import { isEmailEnabled, isForeverSignupEnabled } from '/lib/config.ts';
|
||||
import { AppConfig } from '/lib/config.ts';
|
||||
|
||||
const db = new Database();
|
||||
|
||||
@@ -35,7 +35,7 @@ export class UserModel {
|
||||
}
|
||||
|
||||
static async create(email: User['email'], hashedPassword: User['hashed_password']) {
|
||||
const trialDays = isForeverSignupEnabled() ? 36_525 : 30;
|
||||
const trialDays = await AppConfig.isForeverSignupEnabled() ? 36_525 : 30;
|
||||
const now = new Date();
|
||||
const trialEndDate = new Date(new Date().setUTCDate(new Date().getUTCDate() + trialDays));
|
||||
|
||||
@@ -45,7 +45,7 @@ export class UserModel {
|
||||
updated_at: now.toISOString(),
|
||||
};
|
||||
|
||||
const extra: User['extra'] = { is_email_verified: isEmailEnabled() ? false : true };
|
||||
const extra: User['extra'] = { is_email_verified: (await AppConfig.isEmailVerificationEnabled()) ? false : true };
|
||||
|
||||
// First signup will be an admin "forever"
|
||||
if (!(await this.isThereAnAdmin())) {
|
||||
@@ -65,7 +65,7 @@ export class UserModel {
|
||||
[
|
||||
email,
|
||||
JSON.stringify(subscription),
|
||||
extra.is_admin || isForeverSignupEnabled() ? 'active' : 'trial',
|
||||
(extra.is_admin || (await AppConfig.isForeverSignupEnabled())) ? 'active' : 'trial',
|
||||
hashedPassword,
|
||||
JSON.stringify(extra),
|
||||
],
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'std/dotenv/load.ts';
|
||||
|
||||
import { helpEmail } from '/lib/utils/misc.ts';
|
||||
import { AppConfig } from '/lib/config.ts';
|
||||
|
||||
const BREVO_API_KEY = Deno.env.get('BREVO_API_KEY') || '';
|
||||
|
||||
enum BrevoTemplateId {
|
||||
BEWCLOUD_VERIFY_EMAIL = 20,
|
||||
BEWCLOUD_VERIFY_EMAIL = 20, // NOTE: This will likely be different in your own Brevo account
|
||||
}
|
||||
|
||||
interface BrevoResponse {
|
||||
@@ -43,6 +43,9 @@ async function sendEmailWithTemplate(
|
||||
attachments: BrevoRequestBody['attachment'] = [],
|
||||
cc?: string,
|
||||
) {
|
||||
const config = await AppConfig.getConfig();
|
||||
const helpEmail = config.visuals.helpEmail;
|
||||
|
||||
const email: BrevoRequestBody = {
|
||||
templateId,
|
||||
params: data,
|
||||
|
||||
37
lib/types.ts
37
lib/types.ts
@@ -141,3 +141,40 @@ export const currencyMap = new Map<SupportedCurrencySymbol, SupportedCurrency>([
|
||||
['¥', 'JPY'],
|
||||
['₹', 'INR'],
|
||||
]);
|
||||
|
||||
export type PartialDeep<T> = (T extends (infer U)[] ? PartialDeep<U>[] : { [P in keyof T]?: PartialDeep<T[P]> }) | T;
|
||||
|
||||
export type OptionalApp = 'news' | 'notes' | 'photos' | 'expenses';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
/** The base URL of the application you use to access the app, i.e. "http://localhost:8000" or "https://cloud.example.com" */
|
||||
baseUrl: string;
|
||||
/** If true, anyone can sign up for an account. Note that it's always possible to sign up for the first user, and they will be an admin */
|
||||
allowSignups: boolean;
|
||||
/** If true, email verification will be required for signups (using Brevo) */
|
||||
enableEmailVerification: boolean;
|
||||
/** If true, all signups become active for 100 years */
|
||||
enableForeverSignup: boolean;
|
||||
/** Can be set to allow more than the baseUrl's domain for session cookies */
|
||||
allowedCookieDomains: string[];
|
||||
/** If true, the cookie domain will not be strictly set and checked against. This skipping slightly reduces security, but is usually necessary for reverse proxies like Cloudflare Tunnel. */
|
||||
skipCookieDomainSecurity: boolean;
|
||||
};
|
||||
files: {
|
||||
/** The root-relative root path for files, i.e. "data-files" */
|
||||
rootPath: string;
|
||||
};
|
||||
core: {
|
||||
/** dashboard and files cannot be disabled */
|
||||
enabledApps: OptionalApp[];
|
||||
};
|
||||
visuals: {
|
||||
/** An override title of the application. Empty shows the default title. */
|
||||
title: string;
|
||||
/** An override description of the application. Empty shows the default description. */
|
||||
description: string;
|
||||
/** The email address to contact for help. Empty will disable/hide the "need help" sections. */
|
||||
helpEmail: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
import { currencyMap } from '/lib/types.ts';
|
||||
import { SupportedCurrencySymbol } from '/lib/types.ts';
|
||||
|
||||
let BASE_URL = typeof window !== 'undefined' && window.location
|
||||
? `${window.location.protocol}//${window.location.host}`
|
||||
: '';
|
||||
let CUSTOM_TITLE = '';
|
||||
let CUSTOM_DESCRIPTION = '';
|
||||
let HELP_EMAIL = '';
|
||||
|
||||
if (typeof Deno !== 'undefined') {
|
||||
await import('std/dotenv/load.ts');
|
||||
|
||||
BASE_URL = Deno.env.get('BASE_URL') || '';
|
||||
|
||||
CUSTOM_TITLE = Deno.env.get('CUSTOM_TITLE') || '';
|
||||
CUSTOM_DESCRIPTION = Deno.env.get('CUSTOM_DESCRIPTION') || '';
|
||||
HELP_EMAIL = Deno.env.get('HELP_EMAIL') || '';
|
||||
}
|
||||
|
||||
export const baseUrl = BASE_URL || 'http://localhost:8000';
|
||||
export const defaultTitle = CUSTOM_TITLE || 'bewCloud is a modern and simpler alternative to Nextcloud and ownCloud';
|
||||
export const defaultDescription = CUSTOM_DESCRIPTION || `Have your files under your own control.`;
|
||||
export const helpEmail = HELP_EMAIL;
|
||||
|
||||
export function isRunningLocally(request: Request): boolean {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
|
||||
Reference in New Issue
Block a user