user and pass

This commit is contained in:
Dmitry 2026-01-07 16:01:35 +03:00
parent 25a2b306fe
commit 44c7fce2b7
5 changed files with 276 additions and 2 deletions

View file

@ -55,4 +55,13 @@ export const authApi = {
})
return response.data
},
// Login with username and password
loginWithPassword: async (username: string, password: string): Promise<AuthResponse> => {
const response = await adminApiClient.post('/api/admin/auth/login', {
username,
password,
})
return response.data
},
}

View file

@ -17,9 +17,14 @@ function telegramBotDeepLink(code: string): string {
return `${TELEGRAM_BOT_DEEP_LINK_BASE}?start=${code}`
}
type AuthMethod = 'telegram' | 'password'
export default function LoginPage() {
const navigate = useNavigate()
const { login } = useAuthStore()
const [authMethod, setAuthMethod] = useState<AuthMethod>('telegram')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [code, setCode] = useState('')
const [codeStatus, setCodeStatus] = useState<CodeStatusResponse | null>(null)
const [countdown, setCountdown] = useState<number>(0)
@ -64,6 +69,58 @@ export default function LoginPage() {
},
})
// Login with password mutation
const loginWithPasswordMutation = useMutation({
mutationFn: ({ username, password }: { username: string; password: string }) =>
authApi.loginWithPassword(username, password),
onSuccess: (data) => {
console.log('Login with password response:', data)
if (!data.token || !data.user) {
console.error('Invalid response structure:', data)
toast.error('Invalid response from server')
return
}
console.log('Login successful, setting token and user')
console.log('Token received:', data.token ? `Token exists, length: ${data.token.length}` : 'NO TOKEN!')
console.log('RefreshToken received:', data.refreshToken ? `Token exists, length: ${data.refreshToken.length}` : 'NO REFRESH TOKEN!')
console.log('ExpiresIn:', data.expiresIn)
console.log('User received:', data.user)
// Save token with refresh token and expiration
login(data.token, data.refreshToken ?? null, data.expiresIn ?? null, data.user)
// Verify token was saved
const savedToken = localStorage.getItem('admin_token')
const savedRefreshToken = localStorage.getItem('admin_refresh_token')
console.log('Token saved verification:', savedToken ? `Token exists, length: ${savedToken.length}` : 'Token NOT saved!')
console.log('RefreshToken saved verification:', savedRefreshToken ? `Token exists, length: ${savedRefreshToken.length}` : 'RefreshToken NOT saved!')
if (!savedToken) {
console.error('CRITICAL: Token was not saved to localStorage!')
toast.error('Failed to save authentication token')
return
}
toast.success('Welcome back!')
// Small delay to ensure token is available for subsequent requests
setTimeout(() => {
navigate('/')
}, 100)
},
onError: (error: unknown) => {
console.error('Login with password error:', error)
const errorResponse = error as { response?: { data?: { message?: string }; status?: number } }
const errorMessage =
errorResponse?.response?.data?.message ||
(error as { message?: string })?.message ||
'Invalid credentials'
toast.error(errorMessage)
},
})
// Verify code mutation
const verifyCodeMutation = useMutation({
mutationFn: (code: string) => authApi.verifyCode(code),
@ -270,6 +327,44 @@ export default function LoginPage() {
setCountdown(0)
}
const handlePasswordLogin = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const trimmedUsername = username.trim()
const trimmedPassword = password.trim()
if (!trimmedUsername) {
toast.error('Please enter username')
return
}
if (!trimmedPassword) {
toast.error('Please enter password')
return
}
loginWithPasswordMutation.mutate({
username: trimmedUsername,
password: trimmedPassword,
})
}
const switchAuthMethod = (method: AuthMethod) => {
setAuthMethod(method)
// Reset states when switching
if (method === 'telegram') {
setUsername('')
setPassword('')
handleBack()
} else {
setCode('')
setCodeStatus(null)
setCountdown(0)
stopStatusPolling()
stopCountdown()
setIsVerifying(false)
}
}
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
@ -303,14 +398,77 @@ export default function LoginPage() {
Sto k Odnomu Admin
</CardTitle>
<CardDescription className="text-center">
{codeStatus
{authMethod === 'password'
? 'Enter your credentials to login'
: codeStatus
? 'Send the code to the Telegram bot to continue'
: 'Click the button below to generate a verification code'
}
</CardDescription>
</CardHeader>
<CardContent>
{!codeStatus ? (
{/* Auth method switcher */}
<div className="flex gap-2 mb-6">
<Button
type="button"
variant={authMethod === 'telegram' ? 'default' : 'outline'}
onClick={() => switchAuthMethod('telegram')}
className="flex-1"
>
Telegram
</Button>
<Button
type="button"
variant={authMethod === 'password' ? 'default' : 'outline'}
onClick={() => switchAuthMethod('password')}
className="flex-1"
>
Login / Password
</Button>
</div>
{authMethod === 'password' ? (
<form onSubmit={handlePasswordLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
type="text"
placeholder="Enter username"
value={username}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUsername(e.target.value)
}}
disabled={loginWithPasswordMutation.isPending}
autoComplete="username"
autoFocus
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Enter password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}}
disabled={loginWithPasswordMutation.isPending}
autoComplete="current-password"
/>
</div>
<Button
type="submit"
className="w-full"
disabled={loginWithPasswordMutation.isPending || !username.trim() || !password.trim()}
>
{loginWithPasswordMutation.isPending ? 'Logging in...' : 'Login'}
</Button>
</form>
) : (
<>
{!codeStatus ? (
<div className="space-y-4">
<Button
onClick={handleRequestCode}
@ -431,6 +589,8 @@ export default function LoginPage() {
Open Telegram Bot
</Button>
</div>
)}
</>
)}
</CardContent>
</Card>

View file

@ -10,6 +10,7 @@ import {
import { AdminAuthService } from './admin-auth.service';
import { VerifyCodeDto } from './dto/verify-code.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { LoginDto } from './dto/login.dto';
import { AdminAuthGuard } from '../guards/admin-auth.guard';
import { AdminGuard } from '../guards/admin.guard';
@ -32,6 +33,14 @@ export class AdminAuthController {
return this.adminAuthService.verifyCode(verifyCodeDto.code);
}
@Post('login')
async login(@Body() loginDto: LoginDto) {
return this.adminAuthService.loginWithPassword(
loginDto.username,
loginDto.password,
);
}
@Post('refresh')
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.adminAuthService.refreshAccessToken(

View file

@ -325,4 +325,88 @@ export class AdminAuthService implements OnModuleInit {
getAdminIdsList(): string[] {
return AdminChecker.getAdminIds();
}
/**
* Login with username and password from environment variables
*/
async loginWithPassword(username: string, password: string): Promise<{
token: string;
refreshToken: string;
expiresIn: number;
user: any;
}> {
const adminUsername = this.configService.get<string>('ADMIN_USERNAME');
const adminPassword = this.configService.get<string>('ADMIN_PASSWORD');
if (!adminUsername || !adminPassword) {
throw new UnauthorizedException('Admin credentials not configured');
}
if (username !== adminUsername || password !== adminPassword) {
throw new UnauthorizedException('Invalid credentials');
}
// Find or create admin user for password login
// Use a special telegramId format to identify password-based admin
const passwordAdminTelegramId = 'password_admin';
let user = await this.prisma.user.findUnique({
where: { telegramId: passwordAdminTelegramId },
select: {
id: true,
email: true,
name: true,
role: true,
telegramId: true,
},
});
if (!user) {
// Create admin user for password login
user = await this.prisma.user.create({
data: {
telegramId: passwordAdminTelegramId,
role: 'ADMIN',
name: 'Admin',
email: adminUsername,
},
select: {
id: true,
email: true,
name: true,
role: true,
telegramId: true,
},
});
} else if (user.role !== 'ADMIN') {
// Upgrade to admin if needed
await this.prisma.user.update({
where: { id: user.id },
data: { role: 'ADMIN' },
});
user.role = 'ADMIN';
}
// Generate tokens
const expiresIn = 3600; // 1 hour
const secret = this.configService.get<string>('ADMIN_JWT_SECRET') ||
this.configService.get<string>('JWT_SECRET');
const token = this.jwtService.sign(
{ sub: user.id, email: user.email, role: user.role },
{ secret, expiresIn },
);
const refreshToken = this.jwtService.sign(
{ sub: user.id, type: 'refresh' },
{ secret, expiresIn: 60 * 60 * 24 * 7 }, // 7 days
);
return {
token,
refreshToken,
expiresIn,
user,
};
}
}

View file

@ -0,0 +1,12 @@
import { IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsString()
@MinLength(1)
username: string;
@IsString()
@MinLength(1)
password: string;
}