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
|
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}`
|
return `${TELEGRAM_BOT_DEEP_LINK_BASE}?start=${code}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthMethod = 'telegram' | 'password'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { login } = useAuthStore()
|
const { login } = useAuthStore()
|
||||||
|
const [authMethod, setAuthMethod] = useState<AuthMethod>('telegram')
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
const [code, setCode] = useState('')
|
const [code, setCode] = useState('')
|
||||||
const [codeStatus, setCodeStatus] = useState<CodeStatusResponse | null>(null)
|
const [codeStatus, setCodeStatus] = useState<CodeStatusResponse | null>(null)
|
||||||
const [countdown, setCountdown] = useState<number>(0)
|
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
|
// Verify code mutation
|
||||||
const verifyCodeMutation = useMutation({
|
const verifyCodeMutation = useMutation({
|
||||||
mutationFn: (code: string) => authApi.verifyCode(code),
|
mutationFn: (code: string) => authApi.verifyCode(code),
|
||||||
|
|
@ -270,6 +327,44 @@ export default function LoginPage() {
|
||||||
setCountdown(0)
|
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 formatTime = (seconds: number): string => {
|
||||||
const mins = Math.floor(seconds / 60)
|
const mins = Math.floor(seconds / 60)
|
||||||
const secs = seconds % 60
|
const secs = seconds % 60
|
||||||
|
|
@ -303,14 +398,77 @@ export default function LoginPage() {
|
||||||
Sto k Odnomu Admin
|
Sto k Odnomu Admin
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-center">
|
<CardDescription className="text-center">
|
||||||
{codeStatus
|
{authMethod === 'password'
|
||||||
|
? 'Enter your credentials to login'
|
||||||
|
: codeStatus
|
||||||
? 'Send the code to the Telegram bot to continue'
|
? 'Send the code to the Telegram bot to continue'
|
||||||
: 'Click the button below to generate a verification code'
|
: 'Click the button below to generate a verification code'
|
||||||
}
|
}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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">
|
<div className="space-y-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRequestCode}
|
onClick={handleRequestCode}
|
||||||
|
|
@ -431,6 +589,8 @@ export default function LoginPage() {
|
||||||
Open Telegram Bot
|
Open Telegram Bot
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { AdminAuthService } from './admin-auth.service';
|
import { AdminAuthService } from './admin-auth.service';
|
||||||
import { VerifyCodeDto } from './dto/verify-code.dto';
|
import { VerifyCodeDto } from './dto/verify-code.dto';
|
||||||
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
import { RefreshTokenDto } from './dto/refresh-token.dto';
|
||||||
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { AdminAuthGuard } from '../guards/admin-auth.guard';
|
import { AdminAuthGuard } from '../guards/admin-auth.guard';
|
||||||
import { AdminGuard } from '../guards/admin.guard';
|
import { AdminGuard } from '../guards/admin.guard';
|
||||||
|
|
||||||
|
|
@ -32,6 +33,14 @@ export class AdminAuthController {
|
||||||
return this.adminAuthService.verifyCode(verifyCodeDto.code);
|
return this.adminAuthService.verifyCode(verifyCodeDto.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('login')
|
||||||
|
async login(@Body() loginDto: LoginDto) {
|
||||||
|
return this.adminAuthService.loginWithPassword(
|
||||||
|
loginDto.username,
|
||||||
|
loginDto.password,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||||
return this.adminAuthService.refreshAccessToken(
|
return this.adminAuthService.refreshAccessToken(
|
||||||
|
|
|
||||||
|
|
@ -325,4 +325,88 @@ export class AdminAuthService implements OnModuleInit {
|
||||||
getAdminIdsList(): string[] {
|
getAdminIdsList(): string[] {
|
||||||
return AdminChecker.getAdminIds();
|
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