frontend improvements, yandex model iimplementations
Dev testing pipeline / BuildAndTest (push) Successful in 7m54s Details

This commit is contained in:
Vasya Ryzhkoff 2024-08-11 01:43:00 +07:00
parent e9b91ff4e3
commit 1621f90f0c
29 changed files with 344 additions and 20 deletions

View File

@ -26,6 +26,10 @@ namespace Yuna.Website.Server.API
.Produces(200) .Produces(200)
.Produces(400); .Produces(400);
app.MapPost("/api/auth/logout", Logout)
.WithTags("auth")
.Produces(200);
} }
@ -37,9 +41,18 @@ namespace Yuna.Website.Server.API
var hashedPassword = Encrypter.HashPassword(request.RawPassword, request.UserName); var hashedPassword = Encrypter.HashPassword(request.RawPassword, request.UserName);
if (!hashedPassword.Equals(userFromDb.HashedPassword)) return Results.Unauthorized(); if (!hashedPassword.Equals(userFromDb.HashedPassword)) return Results.Unauthorized();
await SetAccessToken(context, tokenService, userFromDb); await SetAccessToken(context, tokenService, userFromDb);
SetFrontendCookie(userFromDb, context);
return Results.Ok(""); return Results.Ok("");
} }
public async Task<IResult> Logout(HttpContext context, object? additionalInfo = null)
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
context.ClearCookies();
return Results.Ok();
}
private static async Task SetAccessToken(HttpContext context, ITokenService tokenService, User userFromDb) private static async Task SetAccessToken(HttpContext context, ITokenService tokenService, User userFromDb)
{ {

View File

@ -24,13 +24,28 @@ namespace Yuna.Website.Server.Infrastructure
return default!; return default!;
} }
public static void SaveToCookies<T>(this HttpContext context, string key, T value, TimeSpan? maxAge = null) public static void SaveToCookies<T>(this HttpContext context, string key, T value, TimeSpan? maxAge = null, bool httpOnly = false)
{ {
var dataStr = System.Text.Json.JsonSerializer.Serialize<T>(value); var dataStr = System.Text.Json.JsonSerializer.Serialize<T>(value);
if (context.Request.Cookies.ContainsKey(key)) context.Response.Cookies.Delete(key); if (context.Request.Cookies.ContainsKey(key)) context.Response.Cookies.Delete(key);
context.Response.Cookies.Append(key, dataStr, new CookieOptions() { HttpOnly = true, MaxAge = maxAge ?? TimeSpan.FromDays(365) }); var sameSite = !Settings.IsDevEnv;
context.Response.Cookies.Append(key, dataStr, new CookieOptions()
{ HttpOnly = httpOnly,
MaxAge = maxAge ?? TimeSpan.FromDays(365),
SameSite = sameSite ? SameSiteMode.Strict : SameSiteMode.None ,
Secure = true
});
}
public static void ClearCookies(this HttpContext context)
{
foreach (var cookie in context.Request.Cookies)
{
context.Response.Cookies.Delete(cookie.Key);
}
} }
public static long? GetUserIdFromCookie(this HttpContext context) public static long? GetUserIdFromCookie(this HttpContext context)

View File

@ -9,8 +9,11 @@ namespace Yuna.Website.Server.Infrastructure
{ {
Id = user.Id; Id = user.Id;
Username = user.UserName; Username = user.UserName;
IsAdmin = user.IsAdmin;
} }
[JsonPropertyName("isAdmin")]
public bool IsAdmin { get; }
[JsonPropertyName("username")] [JsonPropertyName("username")]
public string Username { get; } public string Username { get; }

View File

@ -10,10 +10,14 @@ namespace Yuna.Website.Server.Infrastructure
public static string ReferalCode { get; private set; } = null!; public static string ReferalCode { get; private set; } = null!;
public static string DbConnectionStr { get; private set; } = null!; public static string DbConnectionStr { get; private set; } = null!;
public static bool IsDevEnv { get; private set; } = false;
public static bool IsProduction => !IsDevEnv;
public static string HttpsExternalUrl { get; private set; } = null!; public static string HttpsExternalUrl { get; private set; } = null!;
public static void Init() public static void Init(bool isDev = true)
{ {
IsDevEnv = isDev;
var jsonText = File.ReadAllText("globalSettings.json"); var jsonText = File.ReadAllText("globalSettings.json");
using JsonDocument document = JsonDocument.Parse(jsonText); using JsonDocument document = JsonDocument.Parse(jsonText);

View File

@ -0,0 +1,12 @@
using Yuna.Website.Server.Model.YandexDevice;
namespace Yuna.Website.Server.Model
{
public class DeviceMapping
{
public YandexDeviceType DeviceType { get; set; }
public Device Device { get; set; }
public Dictionary<long,YandexProp> PropMappings { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace Yuna.Website.Server.Model.YandexDevice
{
public enum YandexDeviceType
{
ClimateSensor = 0
}
}

View File

@ -0,0 +1,30 @@
namespace Yuna.Website.Server.Model.YandexDevice
{
public static class YandexExtensions
{
public static string ToJsonName(this YandexDeviceType type)
{
switch(type)
{
case YandexDeviceType.ClimateSensor:
return "sensor.climate";
default:
return "yuna_error";
}
}
public static string ToJsonName(this YandexPropType type)
{
switch (type)
{
case YandexPropType.Float:
return "float";
default:
return "yuna_error";
}
}
}
}

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace Yuna.Website.Server.Model.YandexDevice
{
public class YandexParameter
{
[JsonPropertyName("instance")]
public string Instance { get; set; }
[JsonPropertyName("unit")]
public string Unit { get; set; }
}
}

View File

@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace Yuna.Website.Server.Model.YandexDevice
{
public class YandexProp
{
public YandexProp()
{ }
public YandexProp(YandexPropType type, bool retrievable, bool reportable, YandexParameter parameter)
{
Type = type;
Retrievable = retrievable;
Reportable = reportable;
Parameter = parameter;
}
[JsonIgnore]
public YandexPropType Type { get; set; }
[JsonPropertyName("type")]
public string JsonPropName => Type.ToJsonName();
[JsonPropertyName("retrievable")]
public bool Retrievable { get; set; } = true;
[JsonPropertyName("reportable")]
public bool Reportable { get; set; } = true;
[JsonPropertyName("parameters")]
public YandexParameter Parameter { get; set; } = null!;
}
}

View File

@ -0,0 +1,7 @@
namespace Yuna.Website.Server.Model.YandexDevice
{
public enum YandexPropType
{
Float = 0,
}
}

View File

@ -0,0 +1,6 @@
namespace Yuna.Website.Server.Services.MappingService
{
public interface IMappingService
{
}
}

View File

@ -18,7 +18,7 @@ namespace Yuna.Website.Server
{ {
public static void LoadConfigs(WebApplicationBuilder builder) public static void LoadConfigs(WebApplicationBuilder builder)
{ {
Settings.Init(); Settings.Init(builder.Environment.IsDevelopment());
} }
/// <summary> /// <summary>
@ -206,8 +206,8 @@ namespace Yuna.Website.Server
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
.AllowCredentials() .AllowCredentials()
.SetIsOriginAllowed(origin => true) // Разрешить все источники .SetIsOriginAllowed(origin => true) // Разрешить все источники
.WithExposedHeaders("Location")); // Разрешить заголовок Location) .WithExposedHeaders("Location")); // Разрешить заголовок Location)
} }

View File

@ -8,7 +8,7 @@
<DockerfileContext>..\..</DockerfileContext> <DockerfileContext>..\..</DockerfileContext>
<SpaRoot>..\yuna.website.client</SpaRoot> <SpaRoot>..\yuna.website.client</SpaRoot>
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand> <SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
<SpaProxyServerUrl>https://localhost:5173</SpaProxyServerUrl> <SpaProxyServerUrl>https://192.168.1.2:5173</SpaProxyServerUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -10,12 +10,14 @@
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.8.2",
"axios": "^1.7.2", "axios": "^1.7.2",
"js-cookie": "^3.0.5",
"mobx": "^6.13.0", "mobx": "^6.13.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.24.1" "react-router-dom": "^6.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.12.0", "@types/node": "^20.12.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
@ -2582,6 +2584,12 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"dev": true
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.6", "version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
@ -4153,6 +4161,14 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true "dev": true
}, },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host 192.168.1.2 --port 5173",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
@ -12,12 +12,14 @@
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.8.2",
"axios": "^1.7.2", "axios": "^1.7.2",
"js-cookie": "^3.0.5",
"mobx": "^6.13.0", "mobx": "^6.13.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.24.1" "react-router-dom": "^6.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.12.0", "@types/node": "^20.12.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
@ -31,4 +33,4 @@
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.3.1" "vite": "^5.3.1"
} }
} }

View File

@ -1,6 +1,18 @@
import { useNavigate } from "react-router-dom";
import "../resources/styles/header.scss" import "../resources/styles/header.scss"
import { yunaGlobal } from "../utils/cookies/Yuna";
import { Button, Icon, IconButton } from "@chakra-ui/react";
import { MdOutlineLogout } from "react-icons/md";
import { LoginPageService } from "../pages/login/LoginPageService";
export const Header = () => { export const Header = () => {
const navigate = useNavigate();
const onLogout = async () => {
await LoginPageService.logout();
navigate("/login");
}
return ( return (
<div style={{ <div style={{
display: 'flex', display: 'flex',
@ -8,6 +20,29 @@ export const Header = () => {
paddingTop: 10 paddingTop: 10
}}> }}>
<header className="header"> <header className="header">
<h1>Yuna</h1> <h1 onClick={() => navigate("/")}>Yuna</h1>
</header></div>); <nav>
<a onClick={() => navigate("/mappings")}>Маппинги</a>
</nav>
<div style={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
paddingRight: '5px'
}}>
<span >{'Приветик, ' + yunaGlobal.user?.username}
<IconButton
icon={<MdOutlineLogout />}
size='sm'
color="black"
colorScheme="white"
aria-label={"logout"}
onClick={() => onLogout()} /></span>
{yunaGlobal.user?.isAdmin && <span
style={{ paddingRight: 10 }}
className="micro-text important" >Ого, ты админ 💅</span>}
</div>
</header>
</div>);
} }

View File

@ -0,0 +1,9 @@
import { Yuna } from "./utils/cookies/Yuna";
declare global {
// eslint-disable-next-line no-var
var yuna: Yuna;
}
export { }

View File

@ -3,6 +3,9 @@ import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom' import { RouterProvider } from 'react-router-dom'
import "./resources/styles/common.scss" import "./resources/styles/common.scss"
import { router } from './router' import { router } from './router'
import { yunaGlobal } from './utils/cookies/Yuna'
globalThis.yuna = yunaGlobal;
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>

View File

@ -5,8 +5,6 @@ import { EditModal } from "./components/modals/EditModal";
export const HomePage = () => { export const HomePage = () => {
return ( return (
<> <>
<Header />
<DeviceList /> <DeviceList />
<EditModal /> <EditModal />
</> </>

View File

@ -11,10 +11,10 @@ export const DeviceList = observer(() => {
}, []) }, [])
return ( return (
< main className="main" > < >
{homePageStore.isLoading ? <LoadingComponent /> : {homePageStore.isLoading ? <LoadingComponent /> :
homePageStore.devices.length > 0 homePageStore.devices.length > 0
&& homePageStore.devices.map(x => <DeviceCard key={x.id} data={x} />) && homePageStore.devices.map(x => <DeviceCard key={x.id} data={x} />)
} }
</main >); </>);
}); });

View File

@ -2,9 +2,29 @@ import { Button, Center, Input, Spinner } from "@chakra-ui/react";
import "../../resources/styles/loginPage.scss" import "../../resources/styles/loginPage.scss"
import { observer } from "mobx-react" import { observer } from "mobx-react"
import { LoginPageStore } from "./LoginPageStore"; import { LoginPageStore } from "./LoginPageStore";
import { useRef } from "react";
const store = new LoginPageStore(); const store = new LoginPageStore();
export const LoginPage = observer(() => { export const LoginPage = observer(() => {
const loginRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const handleKeyDown = (key: string) => {
if (key === "ArrowDown") {
if (document.activeElement === loginRef.current) {
passwordRef.current?.focus();
}
} else if (key === "ArrowUp") {
if (document.activeElement === passwordRef.current) {
loginRef.current?.focus();
}
}
else if (key === "Enter") store.handleLogin();
};
return ( return (
<div className="container_wrapper"> <div className="container_wrapper">
<div className="login_container"> <div className="login_container">
@ -22,14 +42,18 @@ export const LoginPage = observer(() => {
</> </>
: <> : <>
<Input <Input
ref={loginRef}
placeholder="Логин" placeholder="Логин"
value={store.login} value={store.login}
onChange={e => store.setLogin(e.target.value)} onChange={e => store.setLogin(e.target.value)}
onKeyDown={x => handleKeyDown(x.key)}
marginBottom={5} /> marginBottom={5} />
<Input <Input
ref={passwordRef}
placeholder="Пароль" placeholder="Пароль"
type="password" type="password"
marginBottom={7} marginBottom={7}
onKeyDown={x => handleKeyDown(x.key)}
value={store.password} value={store.password}
onChange={e => store.setPassword(e.target.value)} /> onChange={e => store.setPassword(e.target.value)} />
</>} </>}

View File

@ -22,6 +22,12 @@ export class LoginPageService {
} }
} }
public static async logout(): Promise<boolean> {
const response = await api.post("auth/logout");
if (response.status === 200) return true;
return false;
}
public static async loginOauth(request: IOauthLoginRequest): Promise<IOauthLoginResult> { public static async loginOauth(request: IOauthLoginRequest): Promise<IOauthLoginResult> {

View File

@ -7,6 +7,7 @@ import { IOauthQueryParams } from "./types";
export class LoginPageStore { export class LoginPageStore {
toast = createStandaloneToast(); toast = createStandaloneToast();
login: string = ""; login: string = "";
password: string = ""; password: string = "";
@ -84,7 +85,7 @@ export class LoginPageStore {
return; return;
} }
window.location.replace("https://localhost:5173"); window.location.replace("/");
} }

View File

@ -0,0 +1,6 @@
import { observer } from "mobx-react"
export const MappingsPage = observer(() => {
return <>
</>
})

View File

@ -1,5 +1,6 @@
* { * {
font-family: 'Montserrat'; font-family: 'Montserrat';
} }
.container_wrapper { .container_wrapper {
@ -15,4 +16,12 @@
font-display: block; font-display: block;
font-family: 'Montserrat'; font-family: 'Montserrat';
src: url(../fonts/Montserrat-VariableFont_wght.ttf); src: url(../fonts/Montserrat-VariableFont_wght.ttf);
}
.micro-text {
font-size: 10px;
}
.important {
color: red;
} }

View File

@ -2,6 +2,9 @@
position: fixed; position: fixed;
background: #4FD1C5; background: #4FD1C5;
border-radius: 10px; border-radius: 10px;
display: flex;
flex-direction: row;
align-items: center;
//border-bottom: solid black 1px; //border-bottom: solid black 1px;
width: 80%; width: 80%;
height: 70px; height: 70px;
@ -10,10 +13,31 @@
/* x-offset y-offset blur-radius color */ /* x-offset y-offset blur-radius color */
h1 { h1 {
padding-top: 10px; transition: color 0.3s;
font-size: 30px; font-size: 30px;
padding-left: 30px; padding-left: 30px;
} }
h1:hover {
cursor: pointer;
color: white;
}
nav {
display: flex;
flex-direction: row;
padding-top: 10px;
padding-left: 30px;
a {
color: black;
transition: color 0.3s;
}
a:hover {
cursor: pointer;
color: white;
}
}
} }

View File

@ -1,8 +1,10 @@
import { ChakraProvider } from "@chakra-ui/react"; import { ChakraProvider } from "@chakra-ui/react";
import { Outlet, createBrowserRouter } from "react-router-dom"; import { Navigate, Outlet, createBrowserRouter } from "react-router-dom";
import { LoginPage } from "./pages/login/LoginPage"; import { LoginPage } from "./pages/login/LoginPage";
import { YunaTheme } from "./theme"; import { YunaTheme } from "./theme";
import { HomePage } from "./pages/home/HomePage"; import { HomePage } from "./pages/home/HomePage";
import { MappingsPage } from "./pages/mappings/MappingsPage";
import { Header } from "./components/Header";
@ -13,13 +15,31 @@ export const router = createBrowserRouter([
errorElement: <h1>Something gone wrong</h1>, errorElement: <h1>Something gone wrong</h1>,
children: children:
[ [
{
path: "/",
element: <Navigate to="/home" replace />
},
{ {
path: "home", path: "home",
element: <HomePage /> element: <>
<Header />
<main className="main" >
<HomePage />
</main>
</>
},
{
path: "mappings",
element: <>
<Header />
<main className="main" >
<MappingsPage />
</main>
</>
}, },
{ {
path: "settings", path: "settings",
element: <>set</> element: <><Header />set</>
}, },
{ {
path: "login", path: "login",

View File

@ -0,0 +1,26 @@
import Cookies from 'js-cookie';
class Yuna {
getUser() {
const cookieStr = Cookies.get("user");
if (cookieStr) {
const decodedCookie = decodeURIComponent(cookieStr);
return JSON.parse(decodedCookie) as IUserCookie;
}
return undefined;
}
user: IUserCookie | undefined = this.getUser()
}
export const yunaGlobal = new Yuna();
interface IUserCookie {
isAdmin: boolean
username: string,
id: number
}