Merge Tag 'v0.2.31'

This commit is contained in:
2026-03-09 12:32:35 +01:00
22 changed files with 627 additions and 135 deletions

View File

@@ -13,22 +13,16 @@ import { atomFamily, atomWithStorage } from "jotai/utils";
import { DateTime } from "luxon";
import {
BrowserSettings,
CoinsData,
CompletionCache,
getDefaultCoinsData,
getDefaultHabitsData,
getDefaultPublicUsersData,
getDefaultServerSettings,
getDefaultSettings,
getDefaultUsersData,
getDefaultWishlistData,
Habit,
HabitsData,
PomodoroAtom,
ServerSettings,
Settings,
UserData,
UserId,
WishlistData
UserId
} from "./types";
export const browserSettingsAtom = atomWithStorage('browserSettings', {
@@ -37,13 +31,13 @@ export const browserSettingsAtom = atomWithStorage('browserSettings', {
expandedWishlist: false
} as BrowserSettings)
export const usersAtom = atom(getDefaultUsersData<UserData>())
export const usersAtom = atom(getDefaultPublicUsersData())
export const currentUserIdAtom = atom<UserId | undefined>(undefined);
export const settingsAtom = atom(getDefaultSettings<Settings>());
export const habitsAtom = atom(getDefaultHabitsData<HabitsData>());
export const coinsAtom = atom(getDefaultCoinsData<CoinsData>());
export const wishlistAtom = atom(getDefaultWishlistData<WishlistData>());
export const serverSettingsAtom = atom(getDefaultServerSettings<ServerSettings>());
export const settingsAtom = atom(getDefaultSettings());
export const habitsAtom = atom(getDefaultHabitsData());
export const coinsAtom = atom(getDefaultCoinsData());
export const wishlistAtom = atom(getDefaultWishlistData());
export const serverSettingsAtom = atom(getDefaultServerSettings());
export const userSelectAtom = atom<boolean>(false)
export const aboutOpenAtom = atom<boolean>(false)

25
lib/avatar.ts Normal file
View File

@@ -0,0 +1,25 @@
export const ALLOWED_AVATAR_EXTENSIONS = new Set([
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.avif',
])
export const ALLOWED_AVATAR_MIME_TYPES = new Set([
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/avif',
])
export const AVATAR_CONTENT_TYPE: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.avif': 'image/avif',
}

View File

@@ -1,8 +1,19 @@
import { auth } from '@/auth'
import 'server-only'
import { User, UserId } from './types'
import { loadUsersData } from '@/app/actions/data'
import { User, UserData, UserId, getDefaultUsersData } from './types'
import { randomBytes, scryptSync } from 'crypto'
import fs from 'fs/promises'
import path from 'path'
async function loadUsersDataFromStore(): Promise<UserData> {
try {
const filePath = path.join(process.cwd(), 'data', 'auth.json')
const data = await fs.readFile(filePath, 'utf8')
return JSON.parse(data) as UserData
} catch {
return getDefaultUsersData()
}
}
export async function getCurrentUserId(): Promise<UserId | undefined> {
const session = await auth()
@@ -15,7 +26,7 @@ export async function getCurrentUser(): Promise<User | undefined> {
if (!currentUserId) {
return undefined
}
const usersData = await loadUsersData()
const usersData = await loadUsersDataFromStore()
return usersData.users.find((u) => u.id === currentUserId)
}
export function saltAndHashPassword(password: string, salt?: string): string {

View File

@@ -27,6 +27,7 @@ export type SafeUser = SessionUser & {
avatarPath?: string
permissions?: Permission[]
isAdmin?: boolean
hasPassword?: boolean
}
export type User = SafeUser & {
@@ -34,6 +35,10 @@ export type User = SafeUser & {
lastNotificationReadTimestamp?: string // UTC ISO date string
}
export type PublicUser = Omit<User, 'password'> & {
hasPassword: boolean
}
export type Habit = {
id: string
name: string
@@ -81,6 +86,10 @@ export interface UserData {
users: User[]
}
export interface PublicUserData {
users: PublicUser[]
}
export interface HabitsData {
habits: Habit[];
}
@@ -98,7 +107,7 @@ export interface WishlistData {
}
// Default value functions
export function getDefaultUsersData<UserData>(): UserData {
export function getDefaultUsersData(): UserData {
return {
users: [
{
@@ -112,23 +121,30 @@ export function getDefaultUsersData<UserData>(): UserData {
} as UserData;
};
export function getDefaultHabitsData<HabitsData>(): HabitsData {
return { habits: [] } as HabitsData;
}
export const getDefaultPublicUsersData = (): PublicUserData => ({
users: getDefaultUsersData().users.map(({ password, ...user }) => ({
...user,
hasPassword: !!password,
})),
});
export const getDefaultHabitsData = (): HabitsData => ({
habits: []
});
export function getDefaultTasksData<TasksData>(): TasksData {
return { tasks: [] } as TasksData;
};
export function getDefaultCoinsData<CoinsData>(): CoinsData {
export function getDefaultCoinsData(): CoinsData {
return { balance: 0, transactions: [] } as CoinsData;
};
export function getDefaultWishlistData<WishlistData>(): WishlistData {
export function getDefaultWishlistData(): WishlistData {
return { items: [] } as WishlistData;
}
export function getDefaultSettings<Settings>(): Settings {
export function getDefaultSettings(): Settings {
return {
ui: {
useNumberFormatting: true,
@@ -144,12 +160,12 @@ export function getDefaultSettings<Settings>(): Settings {
} as Settings;
};
export function getDefaultServerSettings<ServerSettings>(): ServerSettings {
export function getDefaultServerSettings(): ServerSettings {
return { isDemo: false } as ServerSettings;
}
// Map of data types to their default values
export const DATA_DEFAULTS: { [key: string]: <T>() => T } = {
export const DATA_DEFAULTS = {
wishlist: getDefaultWishlistData,
habits: getDefaultHabitsData,
coins: getDefaultCoinsData,
@@ -195,7 +211,7 @@ export interface JotaiHydrateInitialValues {
coins: CoinsData;
habits: HabitsData;
wishlist: WishlistData;
users: UserData;
users: PublicUserData;
serverSettings: ServerSettings;
}

10
lib/user-sanitizer.ts Normal file
View File

@@ -0,0 +1,10 @@
import { PublicUserData, UserData } from './types'
export function sanitizeUserData(data: UserData): PublicUserData {
return {
users: data.users.map(({ password, ...user }) => ({
...user,
hasPassword: !!password,
})),
}
}

View File

@@ -942,11 +942,11 @@ describe('convertMachineReadableFrequencyToHumanReadable', () => {
})
describe('freshness utilities', () => {
const mockSettings: Settings = getDefaultSettings<Settings>();
const mockHabits: HabitsData = getDefaultHabitsData<HabitsData>();
const mockCoins: CoinsData = getDefaultCoinsData<CoinsData>();
const mockWishlist: WishlistData = getDefaultWishlistData<WishlistData>();
const mockUsers: UserData = getDefaultUsersData<UserData>();
const mockSettings: Settings = getDefaultSettings();
const mockHabits: HabitsData = getDefaultHabitsData();
const mockCoins: CoinsData = getDefaultCoinsData();
const mockWishlist: WishlistData = getDefaultWishlistData();
const mockUsers: UserData = getDefaultUsersData();
// Add a user to mockUsers for more realistic testing
mockUsers.users.push({
@@ -991,11 +991,11 @@ describe('freshness utilities', () => {
});
test('should handle empty data consistently', () => {
const emptySettings = getDefaultSettings<Settings>();
const emptyHabits = getDefaultHabitsData<HabitsData>();
const emptyCoins = getDefaultCoinsData<CoinsData>();
const emptyWishlist = getDefaultWishlistData<WishlistData>();
const emptyUsers = getDefaultUsersData<UserData>();
const emptySettings = getDefaultSettings();
const emptyHabits = getDefaultHabitsData();
const emptyCoins = getDefaultCoinsData();
const emptyWishlist = getDefaultWishlistData();
const emptyUsers = getDefaultUsersData();
const string1 = prepareDataForHashing(emptySettings, emptyHabits, emptyCoins, emptyWishlist, emptyUsers);
const string2 = prepareDataForHashing(emptySettings, emptyHabits, emptyCoins, emptyWishlist, emptyUsers);

View File

@@ -1,5 +1,5 @@
import { toast } from "@/hooks/use-toast"
import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, PomodoroAtom, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types'
import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, PomodoroAtom, PublicUserData, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types'
import * as chrono from 'chrono-node'
import { clsx, type ClassValue } from "clsx"
import { DateTime, DateTimeFormatOptions } from "luxon"
@@ -462,7 +462,7 @@ export function prepareDataForHashing(
habits: HabitsData,
coins: CoinsData,
wishlist: WishlistData,
users: UserData
users: UserData | PublicUserData
): string {
return JSON.stringify({
settings,