user and pass
This commit is contained in:
parent
25a2b306fe
commit
44c7fce2b7
5 changed files with 276 additions and 2 deletions
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
backend/src/admin/auth/dto/login.dto.ts
Normal file
12
backend/src/admin/auth/dto/login.dto.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
username: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
password: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue