feat: add auth
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
/target
|
||||
data.db
|
||||
.env
|
||||
data.db*
|
||||
|
||||
44
.sqlx/query-5b381986f605cdd82b2aaa32cdb1ddbb0c95d602f70ce58761192e11fedfa145.json
generated
Normal file
44
.sqlx/query-5b381986f605cdd82b2aaa32cdb1ddbb0c95d602f70ce58761192e11fedfa145.json
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM tokens WHERE is_refresh = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "is_refresh",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "expires_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5b381986f605cdd82b2aaa32cdb1ddbb0c95d602f70ce58761192e11fedfa145"
|
||||
}
|
||||
13
.zed/settings.json
Normal file
13
.zed/settings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"cargo": {
|
||||
"extraEnv": {
|
||||
"DATABASE_URL": "sqlite://./data.db",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
1091
Cargo.lock
generated
1091
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,5 +6,12 @@ edition = "2024"
|
||||
[dependencies]
|
||||
actix-files = "0.6.9"
|
||||
actix-web = "4.12.1"
|
||||
chrono = "0.4.42"
|
||||
quartz = "0.0.4"
|
||||
reqwest = "0.12.28"
|
||||
scp_core = { path = "./scp_core" }
|
||||
serde = { version = "1.0.228", features = ["serde_derive", "derive"] }
|
||||
serde_derive = "1.0.228"
|
||||
serde_json = "1.0.147"
|
||||
sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-native-tls", "sqlite" ] }
|
||||
tokio = "1.48.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import {
|
||||
IconCamera,
|
||||
IconChartBar,
|
||||
@@ -15,12 +15,12 @@ import {
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react"
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { NavDocuments } from "@/components/nav-documents"
|
||||
import { NavMain } from "@/components/nav-main"
|
||||
import { NavSecondary } from "@/components/nav-secondary"
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
import { NavDocuments } from "@/components/nav-documents";
|
||||
import { NavMain } from "@/components/nav-main";
|
||||
import { NavSecondary } from "@/components/nav-secondary";
|
||||
import { NavUser } from "@/components/nav-user";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -29,14 +29,11 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { User } from "@/models/user";
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
@@ -146,9 +143,18 @@ const data = {
|
||||
icon: IconFileWord,
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch("/api/auth/user");
|
||||
const user = (await response.json()) as User;
|
||||
return user;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="offcanvas" {...props}>
|
||||
<SidebarHeader>
|
||||
@@ -172,8 +178,14 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
<NavUser
|
||||
user={{
|
||||
avatar: `https://i.pravatar.cc/200?u=${user?.id || ""}`,
|
||||
email: user?.email || "",
|
||||
name: user?.name || "",
|
||||
}}
|
||||
/>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,76 @@
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "./ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function SiteHeader() {
|
||||
const [oldLoginState, setOldLoginState] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: pingResult } = useQuery({
|
||||
queryKey: ["ping"],
|
||||
queryFn: async () => {
|
||||
let res = await fetch("/api/scp/ping");
|
||||
return await res.text();
|
||||
return (await res.text()) === "true";
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const { data: isLoggedIn } = useQuery({
|
||||
queryKey: ["is_scp_logged_in"],
|
||||
queryFn: async () => {
|
||||
let res = await fetch("/api/scp/auth/is_logged_in");
|
||||
return (await res.text()) === "true";
|
||||
},
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
if (!oldLoginState) {
|
||||
queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
setOldLoginState(true);
|
||||
}
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
async function startFlow() {
|
||||
let res = await fetch("/api/scp/auth/start_flow");
|
||||
if (res.ok && window) {
|
||||
let url = await res.text();
|
||||
window.open(url, "_blank")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<div className="flex flex-row gap-2 justify-center items-center">
|
||||
<span>Netcup</span>
|
||||
{pingResult === "true" ? (
|
||||
<div className="bg-green-500 rounded-[100%] animate-pulse w-4 h-4 ">
|
||||
|
||||
</div>
|
||||
{pingResult ? (
|
||||
isLoggedIn ? (
|
||||
<div className="bg-green-500 rounded-[100%] animate-pulse w-4 h-4 ">
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-yellow-500 rounded-[100%] animate-pulse w-4 h-4 ">
|
||||
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="bg-red-700 rounded-[100%] animate-pulse w-4 h-4">
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isLoggedIn && pingResult && (
|
||||
<Button variant="outline" size="sm" onClick={startFlow}>
|
||||
Login to Netcup SCP
|
||||
</Button>
|
||||
)}
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
|
||||
14
frontend/src/models/user.ts
Normal file
14
frontend/src/models/user.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface User {
|
||||
autologinTokenUsed: string;
|
||||
sub: string;
|
||||
email_verified: boolean;
|
||||
webservicePasswordUsed: string;
|
||||
secureModeActive: string;
|
||||
roles: string[];
|
||||
name: string;
|
||||
id: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
email: string;
|
||||
}
|
||||
3
migrations/20251225181315_init.down.sql
Normal file
3
migrations/20251225181315_init.down.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE tokens;
|
||||
DROP TABLE job_events;
|
||||
16
migrations/20251225181315_init.up.sql
Normal file
16
migrations/20251225181315_init.up.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
is_refresh INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE job_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_name TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_data TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
187
src/auth.rs
Normal file
187
src/auth.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use actix_web::{Responder, get};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Serializer};
|
||||
use sqlx::query;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::{db, helper, models::auth};
|
||||
|
||||
#[get("/api/scp/auth/is_logged_in")]
|
||||
pub async fn is_scp_logged_in() -> impl Responder {
|
||||
if let Ok(pool) = db::get_pool().await {
|
||||
let token = query!("SELECT * FROM tokens WHERE is_refresh = ?", 1)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
if let Ok(_token) = token {
|
||||
return true.to_string();
|
||||
}
|
||||
}
|
||||
false.to_string()
|
||||
}
|
||||
|
||||
#[get("/api/scp/auth/start_flow")]
|
||||
pub async fn start_flow() -> impl Responder {
|
||||
let url = start_device_flow().await.unwrap();
|
||||
url
|
||||
}
|
||||
|
||||
async fn start_device_flow() -> Result<String, Box<dyn Error>> {
|
||||
let url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/auth/device";
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.post(url)
|
||||
.form(&[("client_id", "scp"), ("scope", "offline_access openid")])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let openid_response = response.json::<auth::OpenidResponse>().await?;
|
||||
|
||||
let _handle = start_polling_thread(openid_response.clone());
|
||||
|
||||
Ok(openid_response.verification_uri_complete)
|
||||
}
|
||||
|
||||
fn start_polling_thread(openid_response: auth::OpenidResponse) -> JoinHandle<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let device_code = openid_response.device_code;
|
||||
let interval = openid_response.interval;
|
||||
let token_url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/token";
|
||||
let expiration_time =
|
||||
SystemTime::now() + Duration::from_secs(openid_response.expires_in as u64);
|
||||
|
||||
let thread = tokio::spawn(async move {
|
||||
loop {
|
||||
if expiration_time <= SystemTime::now() {
|
||||
println!("Token expired!");
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(interval as u64)).await;
|
||||
let response = client
|
||||
.post(token_url)
|
||||
.form(&[
|
||||
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
("device_code", &device_code),
|
||||
("client_id", "scp"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Could not poll login!");
|
||||
|
||||
if !response.status().is_success() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let openid_response = response
|
||||
.json::<auth::OpenidTokenResponse>()
|
||||
.await
|
||||
.expect("Could not deserialize response!");
|
||||
|
||||
let pool = db::get_pool().await.expect("Could not get DB Pool");
|
||||
let token_expiry =
|
||||
SystemTime::now() + Duration::from_secs(openid_response.expires_in as u64);
|
||||
let token_expiry: DateTime<Utc> = token_expiry.into();
|
||||
let token_expiry = token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"INSERT INTO tokens (token, is_refresh,expires_at) VALUES (?, 0, ?)",
|
||||
openid_response.access_token,
|
||||
token_expiry
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not insert token into database");
|
||||
|
||||
let refresh_token_expiry = SystemTime::now() + Duration::from_hours(24 * 30);
|
||||
let refresh_token_expiry: DateTime<Utc> = refresh_token_expiry.into();
|
||||
let refresh_token_expiry = refresh_token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"INSERT INTO tokens (token, is_refresh,expires_at) VALUES (?, 1, ?)",
|
||||
openid_response.refresh_token,
|
||||
refresh_token_expiry
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not insert token into database");
|
||||
}
|
||||
});
|
||||
thread
|
||||
}
|
||||
|
||||
pub async fn create_new_access_token() -> Result<(), Box<dyn Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/token";
|
||||
let refresh_token = helper::get_refresh_token().await?;
|
||||
|
||||
let response = client
|
||||
.post(url)
|
||||
.form(&[
|
||||
("client_id", "scp"),
|
||||
("refresh_token", &refresh_token),
|
||||
("grant_type", "refresh_token"),
|
||||
])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let token_response = response.json::<auth::OpenidTokenResponse>().await?;
|
||||
|
||||
let pool = db::get_pool().await.expect("Could not get DB Pool");
|
||||
let token_expiry = SystemTime::now() + Duration::from_secs(token_response.expires_in as u64);
|
||||
let token_expiry: DateTime<Utc> = token_expiry.into();
|
||||
let token_expiry = token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"INSERT INTO tokens (token, is_refresh,expires_at) VALUES (?, 0, ?)",
|
||||
token_response.access_token,
|
||||
token_expiry
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not insert token into database");
|
||||
|
||||
let refresh_token_expiry = SystemTime::now() + Duration::from_hours(24 * 30);
|
||||
let refresh_token_expiry: DateTime<Utc> = refresh_token_expiry.into();
|
||||
let refresh_token_expiry = refresh_token_expiry.to_rfc3339();
|
||||
|
||||
query!(
|
||||
"UPDATE tokens SET token = ?, expires_at = ? WHERE is_refresh = 1 AND token = ?",
|
||||
token_response.refresh_token,
|
||||
refresh_token_expiry,
|
||||
refresh_token
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Could not update refresh token in database");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/api/auth/user")]
|
||||
pub async fn get_user() -> impl Responder {
|
||||
let client = reqwest::Client::new();
|
||||
let url = "https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect/userinfo";
|
||||
let token = helper::get_access_token()
|
||||
.await
|
||||
.expect("Could not get access token");
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Could not get user info");
|
||||
|
||||
let user = response
|
||||
.json::<auth::User>()
|
||||
.await
|
||||
.expect("Could not parse user info");
|
||||
|
||||
let str: String = serde_json::to_string(&user).unwrap();
|
||||
str
|
||||
}
|
||||
45
src/db.rs
Normal file
45
src/db.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
||||
|
||||
use sqlx::{
|
||||
ConnectOptions, Sqlite, SqlitePool,
|
||||
pool::PoolConnection,
|
||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
||||
};
|
||||
|
||||
static DATABASE_POOL: Mutex<Option<SqlitePool>> = Mutex::new(None);
|
||||
|
||||
pub async fn get_pool() -> Result<SqlitePool, sqlx::Error> {
|
||||
let existing_pool = {
|
||||
let lock = DATABASE_POOL.lock().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
if let Some(pool) = existing_pool {
|
||||
return Ok(pool);
|
||||
}
|
||||
|
||||
let dburl = env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://./data.db".to_string());
|
||||
let mut options = SqliteConnectOptions::from_str(&dburl)?;
|
||||
options = options
|
||||
.create_if_missing(true)
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.busy_timeout(Duration::from_secs(5));
|
||||
|
||||
let pool = SqlitePool::connect_with(options).await?;
|
||||
|
||||
{
|
||||
let mut lock = DATABASE_POOL.lock().unwrap();
|
||||
if lock.is_none() {
|
||||
*lock = Some(pool.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn migrate() -> Result<(), sqlx::Error> {
|
||||
let pool = get_pool().await?;
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
32
src/helper.rs
Normal file
32
src/helper.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::error::Error;
|
||||
|
||||
use scp_core::apis::configuration;
|
||||
use sqlx::query;
|
||||
|
||||
use crate::db;
|
||||
|
||||
pub async fn get_refresh_token() -> Result<String, Box<dyn Error>> {
|
||||
let pool = db::get_pool().await?;
|
||||
let tok = query!("SELECT * FROM tokens WHERE is_refresh = 1 ORDER BY expires_at DESC LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.token;
|
||||
|
||||
Ok(tok)
|
||||
}
|
||||
|
||||
pub async fn get_access_token() -> Result<String, Box<dyn Error>> {
|
||||
let pool = db::get_pool().await?;
|
||||
let tok = query!("SELECT * FROM tokens WHERE is_refresh = 0 ORDER BY expires_at DESC LIMIT 1")
|
||||
.fetch_one(&pool)
|
||||
.await?
|
||||
.token;
|
||||
|
||||
Ok(tok)
|
||||
}
|
||||
|
||||
pub async fn get_authed_api_config() -> Result<configuration::Configuration, Box<dyn Error>> {
|
||||
let mut conf = configuration::Configuration::default();
|
||||
conf.bearer_access_token = Some(get_access_token().await?);
|
||||
Ok(conf)
|
||||
}
|
||||
53
src/jobs.rs
Normal file
53
src/jobs.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use quartz::{Job, Scheduler, Trigger};
|
||||
use sqlx::query;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{auth, db};
|
||||
pub fn init_scheduler(rt: Handle) -> Scheduler {
|
||||
let scheduler = Scheduler::new();
|
||||
|
||||
let trigger_every_5min =
|
||||
Trigger::with_identity("5min_trigger", "default_group").every(Duration::from_mins(5));
|
||||
|
||||
let trigger_every_3min =
|
||||
Trigger::with_identity("3min_trigger", "default_group").every(Duration::from_mins(3));
|
||||
|
||||
let rt_cleanup = rt.clone();
|
||||
let cleanup_tokens_job = Job::with_identity("cleanup_tokens_job", "default_group", move || {
|
||||
let rt = rt_cleanup.clone();
|
||||
rt.spawn(async move {
|
||||
let _ = cleanup_old_tokens().await;
|
||||
});
|
||||
});
|
||||
|
||||
let rt_refresh = rt.clone();
|
||||
let refresh_access_token_job =
|
||||
Job::with_identity("refresh_access_token_job", "default_group", move || {
|
||||
let rt = rt_refresh.clone();
|
||||
rt.spawn(async move {
|
||||
let _ = refresh_access_token().await;
|
||||
});
|
||||
});
|
||||
|
||||
scheduler.schedule_job(cleanup_tokens_job, trigger_every_5min);
|
||||
scheduler.schedule_job(refresh_access_token_job, trigger_every_3min);
|
||||
|
||||
scheduler
|
||||
}
|
||||
|
||||
pub async fn cleanup_old_tokens() -> Result<(), Box<dyn Error>> {
|
||||
let pool = db::get_pool().await?;
|
||||
let now = chrono::Utc::now();
|
||||
let now = now.to_rfc3339();
|
||||
query!("DELETE FROM tokens WHERE expires_at < ?", now)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_access_token() -> Result<(), Box<dyn Error>> {
|
||||
auth::create_new_access_token().await?;
|
||||
Ok(())
|
||||
}
|
||||
24
src/main.rs
24
src/main.rs
@@ -1,10 +1,16 @@
|
||||
use std::{env, error::Error};
|
||||
use std::env;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_web::{App, HttpResponse, HttpServer, Responder, get, web};
|
||||
use scp_core::apis::configuration::Configuration;
|
||||
use scp_core::apis::default_api;
|
||||
|
||||
mod auth;
|
||||
mod db;
|
||||
mod helper;
|
||||
mod jobs;
|
||||
mod models;
|
||||
|
||||
#[get("/api/hello")]
|
||||
async fn hello() -> impl Responder {
|
||||
HttpResponse::Ok().json(serde_json::json!({ "message": "hello" }))
|
||||
@@ -18,7 +24,6 @@ async fn spa_fallback() -> actix_web::Result<actix_files::NamedFile> {
|
||||
async fn ping_netcup() -> impl Responder {
|
||||
let config = Configuration::default();
|
||||
let res = default_api::api_ping_get(&config).await;
|
||||
dbg!(&res);
|
||||
|
||||
res.is_ok().to_string()
|
||||
}
|
||||
@@ -30,10 +35,18 @@ async fn main() -> std::io::Result<()> {
|
||||
.parse::<u16>()
|
||||
.unwrap();
|
||||
|
||||
HttpServer::new(|| {
|
||||
db::migrate().await.expect("Error migrating!");
|
||||
|
||||
let rt_handle = tokio::runtime::Handle::current();
|
||||
let scheduler = jobs::init_scheduler(rt_handle.clone());
|
||||
|
||||
let res = HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(hello)
|
||||
.service(ping_netcup)
|
||||
.service(auth::is_scp_logged_in)
|
||||
.service(auth::start_flow)
|
||||
.service(auth::get_user)
|
||||
.service(
|
||||
Files::new("/", "./frontend/dist")
|
||||
.index_file("index.html")
|
||||
@@ -43,5 +56,8 @@ async fn main() -> std::io::Result<()> {
|
||||
})
|
||||
.bind(("127.0.0.1", port))?
|
||||
.run()
|
||||
.await
|
||||
.await;
|
||||
|
||||
scheduler.shutdown();
|
||||
res
|
||||
}
|
||||
|
||||
60
src/models/auth.rs
Normal file
60
src/models/auth.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
pub struct User {
|
||||
pub autologin_token_used: String,
|
||||
pub sub: String,
|
||||
#[serde(rename = "email_verified")]
|
||||
pub email_verified: bool,
|
||||
pub webservice_password_used: String,
|
||||
pub secure_mode_active: String,
|
||||
pub roles: Vec<String>,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
#[serde(rename = "preferred_username")]
|
||||
pub preferred_username: String,
|
||||
#[serde(rename = "given_name")]
|
||||
pub given_name: String,
|
||||
#[serde(rename = "family_name")]
|
||||
pub family_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenidResponse {
|
||||
#[serde(rename = "device_code")]
|
||||
pub device_code: String,
|
||||
#[serde(rename = "user_code")]
|
||||
pub user_code: String,
|
||||
#[serde(rename = "verification_uri")]
|
||||
pub verification_uri: String,
|
||||
#[serde(rename = "verification_uri_complete")]
|
||||
pub verification_uri_complete: String,
|
||||
#[serde(rename = "expires_in")]
|
||||
pub expires_in: i64,
|
||||
pub interval: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenidTokenResponse {
|
||||
#[serde(rename = "access_token")]
|
||||
pub access_token: String,
|
||||
#[serde(rename = "expires_in")]
|
||||
pub expires_in: i64,
|
||||
#[serde(rename = "refresh_expires_in")]
|
||||
pub refresh_expires_in: i64,
|
||||
#[serde(rename = "refresh_token")]
|
||||
pub refresh_token: String,
|
||||
#[serde(rename = "token_type")]
|
||||
pub token_type: String,
|
||||
#[serde(rename = "not-before-policy")]
|
||||
pub not_before_policy: i64,
|
||||
#[serde(rename = "session_state")]
|
||||
pub session_state: String,
|
||||
pub scope: String,
|
||||
}
|
||||
1
src/models/mod.rs
Normal file
1
src/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod auth;
|
||||
Reference in New Issue
Block a user