How to share a store between multiple components with Elm? - elm

I have a static page with two components.
One in the header that shows a menu handling user preferences/login and signup
One in the main page that is able to display a list of user images or a form about the user profile for instance.
Is there a way to share a state (let say the access_token to the API for instance) between those two components?
I could add ports to each components that would update a localStorage key and send the store to each other components.
But is there a better way for this?

The solution I ended-up with was to use two ports:
port saveStore : String -> Cmd msg
port storeChanged : (String -> msg) -> Sub msg
With decoders and encoders:
serializeStore : AccessToken -> Profile -> Cmd Msg
serializeStore access_token profile =
encodeStore access_token profile
|> saveStore
encodeStore : AccessToken -> Profile -> String
encodeStore access_token profile =
let
encoded =
Encode.object
[ ( "access_token", tokenToString access_token |> Encode.string )
, ( "profile", encodeUserProfile profile )
]
in
Encode.encode 0 encoded
deserializeStore : String -> Maybe Store
deserializeStore =
Decode.decodeString decodeStore >> Result.toMaybe
decodeStore : Decoder Store
decodeStore =
Decode.map2 Store
decodeToken
(Decode.field "profile" decodeProfile)
decodeToken : Decoder AccessToken
decodeToken =
Decode.field "access_token" Decode.string
|> Decode.andThen
(\token ->
stringToToken token
|> Decode.succeed
)
And then I use them to sync my component store and keep a copy on the localStorage:
<script src="app.js"></script>
<script>
// The localStorage key to use to store serialized session data
const storeKey = "store";
const headerElement = document.getElementById("header-app");
const headerApp = Elm.Header.init({
node: element,
flags: {
rawStore: localStorage[storeKey] || ""
}
});
const contentElement = document.getElementById("content-app");
const contentApp = Elm.Content.init({
node: element,
flags: {
rawStore: localStorage[storeKey] || ""
}
});
headerApp.ports.saveStore.subscribe((rawStore) => {
localStorage[storeKey] = rawStore;
contentApp.ports.storeChanged.send(rawStore);
});
contentApp.ports.saveStore.subscribe((rawStore) => {
localStorage[storeKey] = rawStore;
headerApp.ports.storeChanged.send(rawStore);
});
// Ensure session is refreshed when it changes in another tab/window
window.addEventListener("storage", (event) => {
if (event.storageArea === localStorage && event.key === storeKey) {
headerApp.ports.storeChanged.send(event.newValue);
contentApp.ports.storeChanged.send(event.newValue);
}
}, false);
</script>

Related

How do you create an actix-web HttpServer with session-based authentication?

I'm working on an internal API with which approved users can read from and insert into a database. My intention is for this program to run on our local network, and for multiple users or applications to be able to access it.
In its current state it functions as long as the user is running a local instance of the client and does all their work under localhost. However, when the same user attempts to log in from their IP address, the session is not stored and nothing can be accessed. The result is the same when attempting to connect from another computer on the network to a computer running the client.
Before commenting or answering, please be aware that this is my first time implementing authentication. Any mistakes or egregious errors on my part are simply out of ignorance.
My Cargo.toml file includes the following dependencies:
actix-session = { version = "0.7.1", features = ["cookie-session"] }
actix-web = "^4"
argon2 = "0.4.1"
rand_core = "0.6.3"
reqwest = "0.11.11"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
sqlx = { version = "0.6.1", features = ["runtime-actix-rustls", "mysql", "macros"] }
Here are the contents of main.rs:
use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use actix_web::cookie::Key;
use actix_web::web::{get, post, Data, Path};
use actix_web::{HttpResponse, Responder};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let secret_key = Key::generate();
// Load or create a new config file.
// let settings = ...
// Create a connection to the database.
let pool = sqlx::mysql::MySqlPoolOptions::new()
.connect(&format!(
"mysql://{}:{}#{}:{}/mydb",
env!("DB_USER"),
env!("DB_PASS"),
env!("DB_HOST"),
env!("DB_PORT"),
))
.await
.unwrap();
println!(
"Application listening on {}:{}",
settings.host,
settings.port,
);
// Instantiate the application and add routes for each handler.
actix_web::HttpServer::new(move || {
let logger = actix_web::middleware::Logger::default();
actix_web::App::new()
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
.wrap(logger)
.app_data(Data::new(pool.clone()))
/*
Routes that return all rows from a database table.
*/
/*
Routes that return a webpage.
*/
.route("/new", get().to(new))
.route("/login", get().to(login))
.route("/register", get().to(register))
/*
Routes that deal with authentication.
*/
.route("/register", post().to(register_user))
.route("/login", post().to(login_user))
.route("/logout", get().to(logout_user))
/*
Routes that handle POST requests.
*/
})
.bind(format!("{}:{}", settings.host, settings.port))?
.run()
.await
}
The code involving authentication is as follows:
use crate::model::User;
use actix_session::Session;
use actix_web::web::{Data, Form};
use actix_web::{error::ErrorUnauthorized, HttpResponse};
use argon2::password_hash::{rand_core::OsRng, PasswordHasher, SaltString};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use sqlx::{MySql, Pool};
#[derive(serde::Serialize)]
pub struct SessionDetails {
user_id: u32,
}
#[derive(Debug, sqlx::FromRow)]
pub struct AuthorizedUser {
pub id: u32,
pub username: String,
pub password_hash: String,
pub approved: bool,
}
pub fn check_auth(session: &Session) -> Result<u32, actix_web::Error> {
match session.get::<u32>("user_id").unwrap() {
Some(user_id) => Ok(user_id),
None => Err(ErrorUnauthorized("User not logged in.")),
}
}
pub async fn register_user(
data: Form<User>,
pool: Data<Pool<MySql>>,
) -> Result<String, Box<dyn std::error::Error>> {
let data = data.into_inner();
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(data.password.as_bytes(), &salt)
.unwrap()
.to_string();
// Use to verify.
// let parsed_hash = PasswordHash::new(&hash).unwrap();
const INSERT_QUERY: &str =
"INSERT INTO users (username, password_hash) VALUES (?, ?) RETURNING id;";
let fetch_one: Result<(u32,), sqlx::Error> = sqlx::query_as(INSERT_QUERY)
.bind(data.username)
.bind(password_hash)
.fetch_one(&mut pool.acquire().await.unwrap())
.await;
match fetch_one {
Ok((user_id,)) => Ok(user_id.to_string()),
Err(err) => Err(Box::new(err)),
}
}
pub async fn login_user(
session: Session,
data: Form<User>,
pool: Data<Pool<MySql>>,
) -> Result<HttpResponse, Box<dyn std::error::Error>> {
let data = data.into_inner();
let fetched_user: AuthorizedUser = match sqlx::query_as(
"SELECT id, username, password_hash, approved FROM users WHERE username = ?;",
)
.bind(data.username)
.fetch_one(&mut pool.acquire().await?)
.await
{
Ok(fetched_user) => fetched_user,
Err(e) => return Ok(HttpResponse::NotFound().body(format!("{e:?}"))),
};
let parsed_hash = PasswordHash::new(&fetched_user.password_hash).unwrap();
match Argon2::default().verify_password(&data.password.as_bytes(), &parsed_hash) {
Ok(_) => {
if !fetched_user.approved {
return Ok(
HttpResponse::Unauthorized().body("This account has not yet been approved.")
);
}
session.insert("user_id", &fetched_user.id)?;
session.renew();
Ok(HttpResponse::Ok().json(SessionDetails {
user_id: fetched_user.id,
}))
}
Err(_) => Ok(HttpResponse::Unauthorized().body("Incorrect password.")),
}
}
pub async fn logout_user(session: Session) -> HttpResponse {
if check_auth(&session).is_err() {
return HttpResponse::NotFound().body("No user logged in.");
}
session.purge();
HttpResponse::SeeOther()
.append_header(("Location", "/login"))
.body(format!("User logged out successfully."))
}
I've set my client up to run with host 0.0.0.0 on port 80, but with the little networking knowledge I have that's the best I could think to do — I'm lost here. Any help would be greatly appreciated.
As it turns out, the cookie was not being transmitted because our local network is not using https.
Changing this...
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
to the following...
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
.cookie_secure(false)
.build(),
)
solves the issue.

Why the webrtc Ice Candidate types are different from two peer connection

I have a peer connection between the web browser (chrome) and an android app.
When I query the stats to check the type of connections between two peers. I guessed that the information should have been the same, however, I saw that the two results are different.
Can anyone explain it to me?
On android
boolean success = peerConnection.getStats(statsReports -> {
for (StatsReport report : statsReports) {
Log.i(TAG, "report " + report.toString());
}
}, null);
Result on android (I manually filtered the Conn-audio filed)
[googLocalAddress: 192.168.123.13:47063],
[localCandidateId: Cand-b6zAa+KI],
[googLocalCandidateType: local],
[googRemoteAddress: 192.168.123.49:50663],
[remoteCandidateId: Cand-2rJUPD95],
[googRemoteCandidateType: local],
In web browser (chrome)
const reqFields = [
'googLocalAddress',
'googLocalCandidateType',
'googRemoteAddress',
'googRemoteCandidateType'
];
const connectionDetails = {};
peerConnection.getStats(stats => {
// console.log("peer starts", stats.result());
const filtered = stats.result().filter(e => {
return e.id.indexOf('Conn-audio') === 0 && e.stat('googActiveConnection') === 'true';
})[0];
if (!filtered) return resolve({});
reqFields.forEach(e => {
connectionDetails[e.replace('goog', '')] = filtered.stat(e);
});
console.log('1111111', connectionDetails);
});
Web browser result
LocalAddress: "192.168.123.49:50663"
LocalCandidateType: "local"
RemoteAddress: ":47063"
RemoteCandidateType: "prflx"

Using API tags for a library

I'm currently creating a library for an API. The endpoints have optional tags, and so I'm trying to create a way to use them in the functions.
import * as request from "request";
class Api {
key: string;
constructor(userInput: string) {
this.key = userInput;
}
champions(tags: object) {
Object.keys(tags).forEach(function(key) {
console.log(key + " = " + tags[key])
})
request(`https://api.champion.gg/v2/champions?api_key=${this.key}&${tags}`, function (error, response, body) {
if(!error && response.statusCode == 200) {
let info = JSON.parse(body)
}
});
}
}
var test = new Api("key")
test.champions({"champData": ["kda", "damage"], "rawr": ["xd", "lmao"]})
So far, the combining of Object.keys and forEach has allowed me to get the response of champData=kda,damage and rawr=xd,lmao, however, I need to be able to assign these to a variable that's usable in the URL. How can I get this to work?
Another issue that may occur later on is that, between each tag, there needs to be an & symbol, but not at the end. I apologize for throwing multiple problems into one, but because this is my first experience with something like this, I'm having many issues.
You can use Object.entries() and URLSearchParams()
const tags = {a:1, b:2, c:3};
const params = new URLSearchParams();
const key = "def";
Object.entries(tags).forEach(([key, prop]) => params.set(key, prop));
const url = `https://api.champion.gg/v2/champions?api_key=${key}&${params.toString()}`;
console.log(url);

Goodreads Connect - url for user to grant my application access to their account

What is the url that I can direct a user so they can grant my application access to their goodreads account? I have sourced the goodreads docs at https://www.goodreads.com/api and maybe overlooking something I just cant figure it out.
I require the goodreads' user id to 'Get the books on a members shelf'
https://www.goodreads.com/api#shelves.list
My first obstacle is to enable the dialogue to pop up where the users signs into their goodreads account which effectively grants my application access to their goodreads data.
.....
continuation from question - last parse function "TypeError: Cannot read property 'parse' of undefined"
Meteor.methods({
getGoodreads: function () {
var oauth = { callback: 'http://localhost:3000/profile/',
consumer_key: 'keyxkeyx',
consumer_secret: 'secreckeyxxsecreckeyxx'
},
url = 'http://www.goodreads.com/oauth/request_token';
request.post({url:url, oauth:oauth}, function (e, r, body) {
var req_data = qs.parse(body);
var uri = 'http://www.goodreads.com/oauth/authorize'
+ '?' + qs.stringify({oauth_token: req_data.oauth_token});
var auth_data = qs.parse(body),
oauth =
{ consumer_key: 'keyxkeyx'
, consumer_secret: 'secreckeyxxsecreckeyxx'
, token: auth_data.oauth_token
, token_secret: req_data.oauth_token_secret
, verifier: auth_data.oauth_verifier
},
url = 'http://www.goodreads.com/oauth/access_token';
console.log(auth_data); // this successfully prints the oauth_token and oauth_token_secret
request.post({url:url, oauth:oauth}, function (e, r, body) {
var perm_data = new qs.parse(body), // "TypeError: Cannot read property 'parse' of undefined"
oauth =
{ consumer_key: 'keyxkeyx'
, consumer_secret: 'secreckeyxxsecreckeyxx'
, token: perm_data.oauth_token
, token_secret: perm_data.oauth_token_secret
},
url = 'https://www.goodreads.com/topic.xml',
qs = {user_id: perm_data.user_id,
key: 'keyxkeyx'};
request.get({url:url, oauth:oauth, json:true}, function (e, r, user) {
console.log(user)
});
});
});
}
});
The authorization URL for Goodreads is:
http://www.goodreads.com/oauth/authorize
You can review a (Ruby) code example here https://www.goodreads.com/api/oauth_example

PhoneGap FileTransfer with HTTP basic authentication

I'm attempting to upload a file from PhoneGap to a server using the FileTransfer method. I need HTTP basic auth to be enabled for this upload.
Here's the relevant code:
var options = new FileUploadOptions({
fileKey: "file",
params: {
id: my_id,
headers: { 'Authorization': _make_authstr() }
}
});
var ft = new FileTransfer();
ft.upload(image, 'http://locahost:8000/api/upload', success, error, options);
Looking over the PhoneGap source code it appears that I can specify the authorization header by including "headers" in the "params" list as I've done above:
JSONObject headers = params.getJSONObject("headers");
for (Iterator iter = headers.keys(); iter.hasNext();)
{
String headerKey = iter.next().toString();
conn.setRequestProperty(headerKey, headers.getString(headerKey));
}
However, this doesn't seem to actually add the header.
So: is there a way to do HTTP basic auth with PhoneGap's FileTransfer, for both iPhone and Android?
You can add custom headers by adding them to the options rather than the params like so:
authHeaderValue = function(username, password) {
var tok = username + ':' + password;
var hash = btoa(tok);
return "Basic " + hash;
};
options.headers = {'Authorization': authHeaderValue('Bob', '1234') };
The correct location for the headers array is as an immediate child of options. options->headers. Not options->params->headers. Here is an example:
//**************************************************************
//Variables used below:
//1 - image_name: contains the actual name of the image file.
//2 - token: contains authorization token. In my case, JWT.
//3 - UPLOAD_URL: URL to which the file will be uploaded.
//4 - image_full_path - Full path for the picture to be uploaded.
//***************************************************************
var options = {
fileKey: "file",
fileName: 'picture',
chunkedMode: false,
mimeType: "multipart/form-data",
params : {'fileName': image_name}
};
var headers = {'Authorization':token};
//Here is the magic!
options.headers = headers;
//NOTE: I creaed a separate object for headers to better exemplify what
// is going on here. Obviously you can simply add the header entry
// directly to options object above.
$cordovaFileTransfer.upload(UPLOAD_URL, image_full_path, options).then(
function(result) {
//do whatever with the result here.
});
Here is the official documentation: https://github.com/apache/cordova-plugin-file-transfer
You can create a authorization header yourself. But you can also enter the credentials in the url like this:
var username = "test", password = "pass";
var uri = encodeURI("http://"+username + ':' + password +"#localhost:8000/api/upload");
See FileTransfer.js for the implementation (line 45):
function getBasicAuthHeader(urlString) {
var header = null;
// This is changed due to MS Windows doesn't support credentials in http uris
// so we detect them by regexp and strip off from result url
// Proof: http://social.msdn.microsoft.com/Forums/windowsapps/en-US/a327cf3c-f033-4a54-8b7f-03c56ba3203f/windows-foundation-uri-security-problem
if (window.btoa) {
var credentials = getUrlCredentials(urlString);
if (credentials) {
var authHeader = "Authorization";
var authHeaderValue = "Basic " + window.btoa(credentials);
header = {
name : authHeader,
value : authHeaderValue
};
}
}
return header;
}