Compare commits
3 Commits
2b72951e66
...
fix/add-me
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bf6b8c6c9 | |||
| 4759374883 | |||
| cb6e34d5ce |
@@ -153,6 +153,14 @@ export class OrganizationsService {
|
||||
existing.role = data.role;
|
||||
return this.userOrgRepository.save(existing);
|
||||
}
|
||||
// Update password for existing user being added to a new org
|
||||
if (data.password) {
|
||||
const passwordHash = await bcrypt.hash(data.password, 12);
|
||||
await dataSource.query(
|
||||
`UPDATE shared.users SET password_hash = $1 WHERE id = $2`,
|
||||
[passwordHash, userId],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create new user
|
||||
const passwordHash = await bcrypt.hash(data.password, 12);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar, Alert, Button } from '@mantine/core';
|
||||
import { AppShell, Burger, Group, Text, Menu, UnstyledButton, Avatar, Alert, Button, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
IconLogout,
|
||||
@@ -9,9 +9,12 @@ import {
|
||||
IconUserCog,
|
||||
IconUsersGroup,
|
||||
IconEyeOff,
|
||||
IconSun,
|
||||
IconMoon,
|
||||
} from '@tabler/icons-react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { AppTour } from '../onboarding/AppTour';
|
||||
import { OnboardingWizard } from '../onboarding/OnboardingWizard';
|
||||
@@ -20,6 +23,7 @@ import logoSrc from '../../assets/logo.svg';
|
||||
export function AppLayout() {
|
||||
const [opened, { toggle, close }] = useDisclosure();
|
||||
const { user, currentOrg, logout, impersonationOriginal, stopImpersonation } = useAuthStore();
|
||||
const { colorScheme, toggleColorScheme } = usePreferencesStore();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isImpersonating = !!impersonationOriginal;
|
||||
@@ -108,6 +112,16 @@ export function AppLayout() {
|
||||
{currentOrg && (
|
||||
<Text size="sm" c="dimmed">{currentOrg.name}</Text>
|
||||
)}
|
||||
<Tooltip label={colorScheme === 'dark' ? 'Light mode' : 'Dark mode'}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={toggleColorScheme}
|
||||
aria-label="Toggle color scheme"
|
||||
>
|
||||
{colorScheme === 'dark' ? <IconSun size={18} /> : <IconMoon size={18} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Menu shadow="md" width={220}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
|
||||
@@ -10,6 +10,7 @@ import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import { App } from './App';
|
||||
import { theme } from './theme/theme';
|
||||
import { usePreferencesStore } from './stores/preferencesStore';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -21,9 +22,11 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<MantineProvider theme={theme}>
|
||||
function Root() {
|
||||
const colorScheme = usePreferencesStore((s) => s.colorScheme);
|
||||
|
||||
return (
|
||||
<MantineProvider theme={theme} forceColorScheme={colorScheme}>
|
||||
<Notifications position="top-right" />
|
||||
<ModalsProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -33,5 +36,11 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</QueryClientProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Root />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -414,7 +414,6 @@ export function DashboardPage() {
|
||||
<Center h={200}><Loader /></Center>
|
||||
) : (
|
||||
<>
|
||||
<Text size="sm" fw={600} c="dimmed">AI Health Scores</Text>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }}>
|
||||
<HealthScoreCard
|
||||
score={healthScores?.operating || null}
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
IconUser, IconPalette, IconClock, IconBell, IconEye,
|
||||
} from '@tabler/icons-react';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
||||
|
||||
export function UserPreferencesPage() {
|
||||
const { user, currentOrg } = useAuthStore();
|
||||
const { colorScheme, toggleColorScheme } = usePreferencesStore();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
@@ -66,7 +68,10 @@ export function UserPreferencesPage() {
|
||||
<Text size="sm">Dark Mode</Text>
|
||||
<Text size="xs" c="dimmed">Switch to dark color theme</Text>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
<Switch
|
||||
checked={colorScheme === 'dark'}
|
||||
onChange={toggleColorScheme}
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
@@ -76,7 +81,7 @@ export function UserPreferencesPage() {
|
||||
<Switch disabled />
|
||||
</Group>
|
||||
<Divider />
|
||||
<Text size="xs" c="dimmed" ta="center">Display preferences coming in a future release</Text>
|
||||
<Text size="xs" c="dimmed" ta="center">More display preferences coming in a future release</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
|
||||
26
frontend/src/stores/preferencesStore.ts
Normal file
26
frontend/src/stores/preferencesStore.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type ColorScheme = 'light' | 'dark';
|
||||
|
||||
interface PreferencesState {
|
||||
colorScheme: ColorScheme;
|
||||
toggleColorScheme: () => void;
|
||||
setColorScheme: (scheme: ColorScheme) => void;
|
||||
}
|
||||
|
||||
export const usePreferencesStore = create<PreferencesState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
colorScheme: 'light',
|
||||
toggleColorScheme: () =>
|
||||
set((state) => ({
|
||||
colorScheme: state.colorScheme === 'light' ? 'dark' : 'light',
|
||||
})),
|
||||
setColorScheme: (scheme) => set({ colorScheme: scheme }),
|
||||
}),
|
||||
{
|
||||
name: 'ledgeriq-preferences',
|
||||
},
|
||||
),
|
||||
);
|
||||
150
scripts/reset-password.sh
Executable file
150
scripts/reset-password.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# reset-password.sh — Reset a user's password in HOA LedgerIQ
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/reset-password.sh <email> <new-password>
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/reset-password.sh admin@hoaledgeriq.com MyNewPassword123
|
||||
# ./scripts/reset-password.sh admin@sunrisevalley.org SecurePass!
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- Defaults ----
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DB_USER="${POSTGRES_USER:-hoafinance}"
|
||||
DB_NAME="${POSTGRES_DB:-hoafinance}"
|
||||
COMPOSE_CMD="docker compose"
|
||||
|
||||
# If running with the SSL override, detect it
|
||||
if [ -f "$PROJECT_DIR/docker-compose.ssl.yml" ] && \
|
||||
docker compose -f "$PROJECT_DIR/docker-compose.yml" \
|
||||
-f "$PROJECT_DIR/docker-compose.ssl.yml" ps --quiet 2>/dev/null | head -1 | grep -q .; then
|
||||
COMPOSE_CMD="docker compose -f $PROJECT_DIR/docker-compose.yml -f $PROJECT_DIR/docker-compose.ssl.yml"
|
||||
fi
|
||||
|
||||
# ---- Colors ----
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
die() { err "$@"; exit 1; }
|
||||
|
||||
# ---- Helpers ----
|
||||
|
||||
ensure_containers_running() {
|
||||
if ! $COMPOSE_CMD ps postgres 2>/dev/null | grep -q "running\|Up"; then
|
||||
die "PostgreSQL container is not running. Start it with: docker compose up -d postgres"
|
||||
fi
|
||||
if ! $COMPOSE_CMD ps backend 2>/dev/null | grep -q "running\|Up"; then
|
||||
die "Backend container is not running. Start it with: docker compose up -d backend"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- CLI ----
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
HOA LedgerIQ Password Reset
|
||||
|
||||
Usage:
|
||||
$(basename "$0") <email> <new-password>
|
||||
|
||||
Examples:
|
||||
$(basename "$0") admin@hoaledgeriq.com MyNewPassword123
|
||||
$(basename "$0") admin@sunrisevalley.org SecurePass!
|
||||
|
||||
This script:
|
||||
1. Verifies the user exists in the database
|
||||
2. Generates a bcrypt hash using bcryptjs (same library the app uses)
|
||||
3. Updates the password in the database
|
||||
4. Verifies the new hash works
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse args
|
||||
case "${1:-}" in
|
||||
-h|--help|help|"") usage ;;
|
||||
esac
|
||||
|
||||
[ $# -lt 2 ] && die "Usage: $(basename "$0") <email> <new-password>"
|
||||
|
||||
EMAIL="$1"
|
||||
NEW_PASSWORD="$2"
|
||||
|
||||
# Load .env if present
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_DIR/.env"
|
||||
set +a
|
||||
DB_USER="${POSTGRES_USER:-hoafinance}"
|
||||
DB_NAME="${POSTGRES_DB:-hoafinance}"
|
||||
fi
|
||||
|
||||
# Ensure containers are running
|
||||
info "Checking containers ..."
|
||||
ensure_containers_running
|
||||
|
||||
# Verify user exists
|
||||
info "Looking up user: ${EMAIL} ..."
|
||||
USER_RECORD=$($COMPOSE_CMD exec -T postgres psql -U "$DB_USER" -d "$DB_NAME" \
|
||||
-t -A -c "SELECT id, email, first_name, last_name, is_superadmin FROM shared.users WHERE email = '${EMAIL}';" 2>/dev/null)
|
||||
|
||||
if [ -z "$USER_RECORD" ]; then
|
||||
die "No user found with email: ${EMAIL}"
|
||||
fi
|
||||
|
||||
# Parse user info for display
|
||||
IFS='|' read -r USER_ID USER_EMAIL FIRST_NAME LAST_NAME IS_SUPER <<< "$USER_RECORD"
|
||||
info "Found user: ${FIRST_NAME} ${LAST_NAME} (${USER_EMAIL})"
|
||||
if [ "$IS_SUPER" = "t" ]; then
|
||||
warn "This is a superadmin account"
|
||||
fi
|
||||
|
||||
# Generate bcrypt hash using bcryptjs inside the backend container
|
||||
info "Generating bcrypt hash ..."
|
||||
HASH=$($COMPOSE_CMD exec -T backend node -e "
|
||||
const bcrypt = require('bcryptjs');
|
||||
bcrypt.hash(process.argv[1], 12).then(h => process.stdout.write(h));
|
||||
" "$NEW_PASSWORD" 2>/dev/null)
|
||||
|
||||
if [ -z "$HASH" ] || [ ${#HASH} -lt 50 ]; then
|
||||
die "Failed to generate bcrypt hash. Is the backend container running?"
|
||||
fi
|
||||
|
||||
# Update the password using a heredoc to avoid shell escaping issues with $ in hashes
|
||||
info "Updating password ..."
|
||||
UPDATE_RESULT=$($COMPOSE_CMD exec -T postgres psql -U "$DB_USER" -d "$DB_NAME" -t -A <<EOSQL
|
||||
UPDATE shared.users SET password_hash = '${HASH}', updated_at = NOW() WHERE email = '${EMAIL}';
|
||||
EOSQL
|
||||
)
|
||||
|
||||
if [[ "$UPDATE_RESULT" != *"UPDATE 1"* ]]; then
|
||||
die "Password update failed. Result: ${UPDATE_RESULT}"
|
||||
fi
|
||||
|
||||
# Verify the new hash works
|
||||
info "Verifying new password ..."
|
||||
VERIFY=$($COMPOSE_CMD exec -T backend node -e "
|
||||
const bcrypt = require('bcryptjs');
|
||||
bcrypt.compare(process.argv[1], process.argv[2]).then(r => process.stdout.write(String(r)));
|
||||
" "$NEW_PASSWORD" "$HASH" 2>/dev/null)
|
||||
|
||||
if [ "$VERIFY" != "true" ]; then
|
||||
die "Verification failed — the hash does not match the password. Something went wrong."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "Password reset successful!"
|
||||
echo ""
|
||||
info " User: ${FIRST_NAME} ${LAST_NAME} (${USER_EMAIL})"
|
||||
info " Login: ${EMAIL}"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user