Add switches to enable some more privacy, fix all errors in osmAuth

This commit is contained in:
Pieter Vander Vennet 2024-05-06 18:58:19 +02:00
parent a856d8edc9
commit fbf23b6e18
6 changed files with 128 additions and 132 deletions

View file

@ -1,4 +1,3 @@
// @ts-ignore
import { osmAuth } from "osm-auth"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import { OsmPreferences } from "./OsmPreferences"
@ -6,7 +5,18 @@ import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { AuthConfig } from "./AuthConfig"
import Constants from "../../Models/Constants"
import OSMAuthInstance = OSMAuth.OSMAuthInstance
interface OsmUserInfo {
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}
export default class UserDetails {
public loggedIn = false
@ -31,7 +41,7 @@ export default class UserDetails {
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
export class OsmConnection {
public auth: OSMAuthInstance
public auth: osmAuth
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
@ -49,7 +59,7 @@ export class OsmConnection {
private readonly _dryRun: Store<boolean>
private readonly fakeUser: boolean
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
private readonly _iframeMode: Boolean | boolean
private readonly _iframeMode: boolean
private readonly _singlePage: boolean
private isChecking = false
private readonly _doCheckRegularly
@ -99,20 +109,19 @@ export class OsmConnection {
ud.languages = ["en"]
this.loadingStatus.setData("logged-in")
}
const self = this
this.UpdateCapabilities()
this.isLoggedIn = this.userDetails.map(
(user) =>
user.loggedIn &&
(self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"),
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
[this.apiIsOnline]
)
this.isLoggedIn.addCallback((isLoggedIn) => {
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
// This means someone attempted to toggle this; so we attempt to login!
self.AttemptLogin()
this.AttemptLogin()
}
})
@ -120,17 +129,16 @@ export class OsmConnection {
this.updateAuthObject()
if (!this.fakeUser) {
self.CheckForMessagesContinuously()
this.CheckForMessagesContinuously()
}
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data)
const self = this
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
console.log("Bootstrap token called back", err, result)
self.AttemptLogin()
this.AttemptLogin()
})
options.oauth_token.setData(undefined)
@ -142,15 +150,15 @@ export class OsmConnection {
}
}
public GetPreference(
public GetPreference<T extends string = string>(
key: string,
defaultValue: string = undefined,
options?: {
documentation?: string
prefix?: string
}
): UIEventSource<string> {
return this.preferencesHandler.GetPreference(key, defaultValue, options)
): UIEventSource<T | undefined> {
return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options)
}
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
@ -192,7 +200,7 @@ export class OsmConnection {
console.log("AttemptLogin called, but ignored as fakeUser is set")
return
}
const self = this
console.log("Trying to log in...")
this.updateAuthObject()
@ -202,33 +210,33 @@ export class OsmConnection {
this.auth.xhr(
{
method: "GET",
path: "/api/0.6/user/details",
path: "/api/0.6/user/details"
},
function (err, details: XMLDocument) {
(err, details: XMLDocument) => {
if (err != null) {
console.log("Could not login due to:", err)
self.loadingStatus.setData("error")
this.loadingStatus.setData("error")
if (err.status == 401) {
console.log("Clearing tokens...")
// Not authorized - our token probably got revoked
self.auth.logout()
self.LogOut()
this.auth.logout()
this.LogOut()
} else {
console.log("Other error. Status:", err.status)
self.apiIsOnline.setData("unreachable")
this.apiIsOnline.setData("unreachable")
}
return
}
if (details == null) {
self.loadingStatus.setData("error")
this.loadingStatus.setData("error")
return
}
// details is an XML DOM of user details
let userInfo = details.getElementsByTagName("user")[0]
const userInfo = details.getElementsByTagName("user")[0]
let data = self.userDetails.data
const data = this.userDetails.data
data.loggedIn = true
console.log("Login completed, userinfo is ", userInfo)
data.name = userInfo.getAttribute("display_name")
@ -261,18 +269,18 @@ export class OsmConnection {
data.home = { lat: lat, lon: lon }
}
self.loadingStatus.setData("logged-in")
this.loadingStatus.setData("logged-in")
const messages = userInfo
.getElementsByTagName("messages")[0]
.getElementsByTagName("received")[0]
data.unreadMessages = parseInt(messages.getAttribute("unread"))
data.totalMessages = parseInt(messages.getAttribute("count"))
self.userDetails.ping()
for (const action of self._onLoggedIn) {
action(self.userDetails.data)
this.userDetails.ping()
for (const action of this._onLoggedIn) {
action(this.userDetails.data)
}
self._onLoggedIn = []
this._onLoggedIn = []
}
)
}
@ -289,11 +297,11 @@ export class OsmConnection {
public async interact(
path: string,
method: "GET" | "POST" | "PUT" | "DELETE",
header?: Record<string, string | number>,
header?: Record<string, string>,
content?: string,
allowAnonymous: boolean = false
): Promise<string> {
let connection: OSMAuthInstance = this.auth
const connection: osmAuth = this.auth
if (allowAnonymous && !this.auth.authenticated()) {
const possibleResult = await Utils.downloadAdvanced(
`${this.Backend()}/api/0.6/${path}`,
@ -310,15 +318,13 @@ export class OsmConnection {
return new Promise((ok, error) => {
connection.xhr(
<any>{
{
method,
options: {
header,
},
headers: header,
content,
path: `/api/0.6/${path}`,
path: `/api/0.6/${path}`
},
function (err, response) {
function(err, response) {
if (err !== null) {
error(err)
} else {
@ -329,32 +335,32 @@ export class OsmConnection {
})
}
public async post(
public async post<T extends string>(
path: string,
content?: string,
header?: Record<string, string | number>,
header?: Record<string, string>,
allowAnonymous: boolean = false
): Promise<any> {
return await this.interact(path, "POST", header, content, allowAnonymous)
): Promise<T> {
return <T> await this.interact(path, "POST", header, content, allowAnonymous)
}
public async put(
public async put<T extends string>(
path: string,
content?: string,
header?: Record<string, string | number>
): Promise<any> {
return await this.interact(path, "PUT", header, content)
header?: Record<string, string>
): Promise<T> {
return <T> await this.interact(path, "PUT", header, content)
}
public async get(
path: string,
header?: Record<string, string | number>,
header?: Record<string, string>,
allowAnonymous: boolean = false
): Promise<string> {
return await this.interact(path, "GET", header, undefined, allowAnonymous)
}
public closeNote(id: number | string, text?: string): Promise<void> {
public closeNote(id: number | string, text?: string): Promise<string> {
let textSuffix = ""
if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text)
@ -362,17 +368,17 @@ export class OsmConnection {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
return new Promise((ok) => {
ok()
ok("")
})
}
return this.post(`notes/${id}/close${textSuffix}`)
}
public reopenNote(id: number | string, text?: string): Promise<void> {
public reopenNote(id: number | string, text?: string): Promise<string> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
return new Promise((ok) => {
ok()
return new Promise(resolve => {
resolve("")
})
}
let textSuffix = ""
@ -398,7 +404,7 @@ export class OsmConnection {
"notes.json",
content,
{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
true
)
@ -439,7 +445,7 @@ export class OsmConnection {
file: gpx,
description: options.description,
tags: options.labels?.join(",") ?? "",
visibility: options.visibility,
visibility: options.visibility
}
if (!contents.description) {
@ -447,9 +453,9 @@ export class OsmConnection {
}
const extras = {
file:
'; filename="' +
"; filename=\"" +
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
'"\r\nContent-Type: application/gpx+xml',
"\"\r\nContent-Type: application/gpx+xml"
}
const boundary = "987654"
@ -457,7 +463,7 @@ export class OsmConnection {
let body = ""
for (const key in contents) {
body += "--" + boundary + "\r\n"
body += 'Content-Disposition: form-data; name="' + key + '"'
body += "Content-Disposition: form-data; name=\"" + key + "\""
if (extras[key] !== undefined) {
body += extras[key]
}
@ -468,7 +474,7 @@ export class OsmConnection {
const response = await this.post("gpx/create", body, {
"Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": body.length,
"Content-Length": ""+body.length
})
const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed)
@ -491,9 +497,9 @@ export class OsmConnection {
{
method: "POST",
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
},
function (err, _) {
function(err) {
if (err !== null) {
error(err)
} else {
@ -508,7 +514,7 @@ export class OsmConnection {
* To be called by land.html
*/
public finishLogin(callback: (previousURL: string) => void) {
this.auth.authenticate(function () {
this.auth.authenticate(function() {
// Fully authed at this point
console.log("Authentication successful!")
const previousLocation = LocalStorageSource.Get("location_before_login")
@ -517,28 +523,6 @@ export class OsmConnection {
}
private updateAuthObject() {
let pwaStandAloneMode = false
try {
if (Utils.runningFromConsole) {
pwaStandAloneMode = true
} else if (
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches
) {
pwaStandAloneMode = true
}
} catch (e) {
console.warn(
"Detecting standalone mode failed",
e,
". Assuming in browser and not worrying furhter"
)
}
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
// Same for an iframe...
this.auth = new osmAuth({
client_id: this._oauth_config.oauth_client_id,
url: this._oauth_config.url,
@ -546,23 +530,22 @@ export class OsmConnection {
redirect_uri: Utils.runningFromConsole
? "https://mapcomplete.org/land.html"
: window.location.protocol + "//" + window.location.host + "/land.html",
singlepage: true,
auto: true,
singlepage: true, // We always use 'singlePage', it is the most stable - including in PWA
auto: true
})
}
private CheckForMessagesContinuously() {
const self = this
if (this.isChecking) {
return
}
Stores.Chronic(3 * 1000).addCallback((_) => {
if (!(self.apiIsOnline.data === "unreachable" || self.apiIsOnline.data === "offline")) {
Stores.Chronic(3 * 1000).addCallback(() => {
if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
return
}
try {
console.log("Api is offline - trying to reconnect...")
self.AttemptLogin()
this.AttemptLogin()
} catch (e) {
console.log("Could not login due to", e)
}
@ -571,10 +554,10 @@ export class OsmConnection {
if (!this._doCheckRegularly) {
return
}
Stores.Chronic(60 * 5 * 1000).addCallback((_) => {
if (self.isLoggedIn.data) {
Stores.Chronic(60 * 5 * 1000).addCallback(() => {
if (this.isLoggedIn.data) {
try {
self.AttemptLogin()
this.AttemptLogin()
} catch (e) {
console.log("Could not login due to", e)
}
@ -592,19 +575,9 @@ export class OsmConnection {
})
}
private readonly _userInfoCache: Record<number, any> = {}
private readonly _userInfoCache: Record<number, OsmUserInfo> = {}
public async getInformationAboutUser(id: number): Promise<{
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}> {
public async getInformationAboutUser(id: number): Promise<OsmUserInfo> {
if (id === undefined) {
return undefined
}

View file

@ -43,6 +43,7 @@ export default class UserRelatedState {
public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
public readonly homeLocation: FeatureSource
public readonly morePrivacy: UIEventSource<undefined | "yes" | "no">
/**
* The language as saved into the preferences of the user, if logged in.
* Note that this is _different_ from the languages a user can set via the osm.org interface here: https://www.openstreetmap.org/preferences
@ -106,12 +107,12 @@ export default class UserRelatedState {
})
)
this.language = this.osmConnection.GetPreference("language")
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
this.showCrosshair = <UIEventSource<any>>this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
this.a11y = <UIEventSource<"always" | "never" | "default">>(
this.osmConnection.GetPreference("a11y")
)
this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.a11y = this.osmConnection.GetPreference("a11y")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove"),

View file

@ -564,5 +564,5 @@ export interface LayerConfigJson {
* ifunset: Write 'change_within_x_m' as usual and if GPS is enabled
* iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey
*/
enableMorePrivacy: boolean
enableMorePrivacy?: boolean
}